From patchwork Fri May 6 14:54:08 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15812 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id D0458C0F2A for ; Fri, 6 May 2022 14:55:14 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 02CD16564D; Fri, 6 May 2022 16:55:13 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651848913; bh=aeyKyDy+MTejulKLqpdZVj/3qJESbh0l+vD0RC7rn58=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=ZuhzZOGJ8BE8Z0teJfKTU5qRbNtGY4gnYzNX24voL/G+k2rO5IbYnaZpW8TZMN9pu 0+RruCPjAmiF70NIZJUCKaen/yjyfh7xGwJJMWRFnrmOGV4wdRTzmhxaYCPrfUpHNK Tw8sZQ2e3i8/f6BI5f5hKpc0RZtlGwcyJPSzC+ZfwNYX/auZXLaEMxMy/OJixQz8m/ to70NWcr+5iJ6Mn6DsbB5V+a4tX2tXEg36VOvF058y1EU/HvxoglYroZ6UNsmiyg+P Y7Bq8lZXG0NcuwNqHsw4aJIk4eV5sV5dU/X8nlU9uDaXTc1I7i0hR93vUohfzsX1q1 LjNLxzavDgY8A== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 9F6EF604A3 for ; Fri, 6 May 2022 16:55:10 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="YUdgwh48"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id B50C0835; Fri, 6 May 2022 16:55:09 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651848910; bh=aeyKyDy+MTejulKLqpdZVj/3qJESbh0l+vD0RC7rn58=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=YUdgwh48q6Idxac17g6uESKcdvIAdLVJAdXS5s8T9pqT80W+z7UrFd5oyzK9vzPr9 KjEPIMmQWEvGHvO17KOG9IjmRS0vJUJ5gLcJTmc3LRydZJ+KHcX07eWfuTcmod+ico pEVMhE7UcK3ljdkPHu0Xy89/kvegp3lP3beera0E= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Fri, 6 May 2022 17:54:08 +0300 Message-Id: <20220506145414.99039-2-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220506145414.99039-1-tomi.valkeinen@ideasonboard.com> References: <20220506145414.99039-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v8 1/7] meson: require meson 0.56+ X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Ubuntu 22.04 LTS has been released with meson 0.61.2, and it is easy to install a recent version of meson with python-pip, so let's update the required meson version to get rid of the Android compilation limitation. Additionally, going to meson 0.55 gives the ability to have patch files for git-wraps which is useful for Python bindings. 0.56 brings meson.project_source_root() and meson.project_build_root(), allowing us to get rid of the deprecated meson.source_root() and meson.build_root(). So, let's update the required meson version to 0.56. Signed-off-by: Tomi Valkeinen Reviewed-by: Kieran Bingham Reviewed-by: Laurent Pinchart --- README.rst | 4 +--- meson.build | 6 +----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index aae6b79f..ae5ede17 100644 --- a/README.rst +++ b/README.rst @@ -47,9 +47,7 @@ A C++ toolchain: [required] Either {g++, clang} Meson Build system: [required] - meson (>= 0.53) ninja-build pkg-config - - meson (>= 0.55) is required for building Android (-Dandroid=enabled) + meson (>= 0.56) ninja-build pkg-config If your distribution doesn't provide a recent enough version of meson, you can install or upgrade it using pip3. diff --git a/meson.build b/meson.build index 29d8542d..b892ba84 100644 --- a/meson.build +++ b/meson.build @@ -1,11 +1,7 @@ # SPDX-License-Identifier: CC0-1.0 project('libcamera', 'c', 'cpp', - # Use of the Android component requires meson 0.55, but Ubuntu 20.04 LTS - # ships meson 0.53. Improve the Ubuntu experience at the expense of - # Android as the former is a much more common use case than the latter at - # this point. This should be fixed after Ubuntu releases 22.04 LTS. - meson_version : '>= 0.53', + meson_version : '>= 0.56', version : '0.0.0', default_options : [ 'werror=true', From patchwork Fri May 6 14:54:09 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15813 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 63B38C0F2A for ; Fri, 6 May 2022 14:55:16 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 773AB65650; Fri, 6 May 2022 16:55:13 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651848913; bh=BOLOXjA2NDmdon5i/FKXQOPu02sVc9c2DtzINraAmgU=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=czxwPoX7NGIHz5KeWHx2eA6Ay61HZ1GeVzSBF1if4b1qcN1kYJlk/t0WczldIxBD7 ycdAt904//3SbmgBciabrIZ2zOIWThGUr1d2OWSBIFpkQ633SRhwDVIQPaV1nSpDUH pCmkdNHitJ8SgFPrQJsHG04wg5YNLmozp8OJJ8BBtobk0t/dXLZ+XzDAhbjsDHqv0D 0BkQtiieVME0HOFfjF0Dlhxei2vkH/EkPrmc9GzQbxkTCBEdToAZt1X/EcA+Q5xSMC r3Lv9G/CWSOfUkZKz4Jc2cS/NDDhGQ6I2nKzKApl5oKsS+d2HB/0+nBMrzA0cNjSMt fhVf/kq2Tsdgg== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id DB0E165644 for ; Fri, 6 May 2022 16:55:10 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="eEkHoqrO"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 513A1A43; Fri, 6 May 2022 16:55:10 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651848910; bh=BOLOXjA2NDmdon5i/FKXQOPu02sVc9c2DtzINraAmgU=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=eEkHoqrO6O0rZ1ThMB+g/Dj2vnNzeWMbRS7pSE0OC1Q+FdSmyV8pG/AF0NB1gPo0g x6Wzp7pCz973QN/WGg2L81WAz1gHCPAHW5+tyyNXA6QPjDExVIyxsSr6Vkub9QhKV2 WItY3e9GLFXjNb5zW35TH9ZYRvlxjESuAiitCSXo= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Fri, 6 May 2022 17:54:09 +0300 Message-Id: <20220506145414.99039-3-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220506145414.99039-1-tomi.valkeinen@ideasonboard.com> References: <20220506145414.99039-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v8 2/7] meson: use new project_*_root() functions X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" meson.source_root() and meson.build_root() are deprecated. Use meson.project_source_root() and meson.project_build_root() instead. Signed-off-by: Tomi Valkeinen Reviewed-by: Kieran Bingham Reviewed-by: Laurent Pinchart --- Documentation/meson.build | 4 ++-- include/libcamera/ipa/meson.build | 8 ++++---- meson.build | 6 +++--- src/libcamera/meson.build | 4 ++-- .../include/libcamera/ipa/meson.build | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Documentation/meson.build b/Documentation/meson.build index 33af82aa..8e2eacc6 100644 --- a/Documentation/meson.build +++ b/Documentation/meson.build @@ -12,8 +12,8 @@ dot = find_program('dot', required : get_option('documentation')) if doxygen.found() and dot.found() cdata = configuration_data() cdata.set('VERSION', 'v@0@'.format(libcamera_git_version)) - cdata.set('TOP_SRCDIR', meson.source_root()) - cdata.set('TOP_BUILDDIR', meson.build_root()) + cdata.set('TOP_SRCDIR', meson.project_source_root()) + cdata.set('TOP_BUILDDIR', meson.project_build_root()) doxyfile = configure_file(input : 'Doxyfile.in', output : 'Doxyfile', diff --git a/include/libcamera/ipa/meson.build b/include/libcamera/ipa/meson.build index 6ea94fb5..442ca3dd 100644 --- a/include/libcamera/ipa/meson.build +++ b/include/libcamera/ipa/meson.build @@ -25,8 +25,8 @@ ipa_mojom_core = custom_target(core_mojom_file.split('.')[0] + '_mojom_module', output : core_mojom_file + '-module', command : [ mojom_parser, - '--output-root', meson.build_root(), - '--input-root', meson.source_root(), + '--output-root', meson.project_build_root(), + '--input-root', meson.project_source_root(), '--mojoms', '@INPUT@' ]) @@ -89,8 +89,8 @@ foreach file : ipa_mojom_files depends : ipa_mojom_core, command : [ mojom_parser, - '--output-root', meson.build_root(), - '--input-root', meson.source_root(), + '--output-root', meson.project_build_root(), + '--input-root', meson.project_source_root(), '--mojoms', '@INPUT@' ]) diff --git a/meson.build b/meson.build index b892ba84..10ad8c5c 100644 --- a/meson.build +++ b/meson.build @@ -17,8 +17,8 @@ project('libcamera', 'c', 'cpp', # git version tag, the build metadata (e.g. +211-c94a24f4) is omitted from # libcamera_git_version. libcamera_git_version = run_command('utils/gen-version.sh', - meson.build_root(), - meson.source_root()).stdout().strip() + meson.project_build_root(), + meson.project_source_root()).stdout().strip() if libcamera_git_version == '' libcamera_git_version = meson.project_version() endif @@ -160,7 +160,7 @@ endif # Create a symlink from the build root to the source root. This is used when # running libcamera from the build directory to locate resources in the source # directory (such as IPA configuration files). -run_command('ln', '-fsT', meson.source_root(), meson.build_root() / 'source') +run_command('ln', '-fsT', meson.project_source_root(), meson.project_build_root() / 'source') configure_file(output : 'config.h', configuration : config_h) diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build index 26912ca1..d6a78760 100644 --- a/src/libcamera/meson.build +++ b/src/libcamera/meson.build @@ -98,11 +98,11 @@ endforeach libcamera_sources += control_sources -gen_version = meson.source_root() / 'utils' / 'gen-version.sh' +gen_version = meson.project_source_root() / 'utils' / 'gen-version.sh' # Use vcs_tag() and not configure_file() or run_command(), to ensure that the # version gets updated with every ninja build and not just at meson setup time. -version_cpp = vcs_tag(command : [gen_version, meson.build_root(), meson.source_root()], +version_cpp = vcs_tag(command : [gen_version, meson.project_build_root(), meson.project_source_root()], input : 'version.cpp.in', output : 'version.cpp', fallback : meson.project_version()) diff --git a/test/serialization/generated_serializer/include/libcamera/ipa/meson.build b/test/serialization/generated_serializer/include/libcamera/ipa/meson.build index ba198f7a..6f8794c1 100644 --- a/test/serialization/generated_serializer/include/libcamera/ipa/meson.build +++ b/test/serialization/generated_serializer/include/libcamera/ipa/meson.build @@ -6,8 +6,8 @@ mojom = custom_target('test_mojom_module', output : 'test.mojom-module', command : [ mojom_parser, - '--output-root', meson.build_root(), - '--input-root', meson.source_root(), + '--output-root', meson.project_build_root(), + '--input-root', meson.project_source_root(), '--mojoms', '@INPUT@' ]) From patchwork Fri May 6 14:54:10 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15814 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 3AFBDC326C for ; Fri, 6 May 2022 14:55:17 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id EE96165653; Fri, 6 May 2022 16:55:14 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651848915; bh=O45OCn0MzoZDSEqbqEq61DIKMlzSOl/dIVbUpTmGN3U=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=FF+kk1MAWoReSenaaTa/89JM4yVN7HPHT3mFav2aSc8DbMqRzCoMXtAYerQfImtwe X5nwIl+FYmJBRZs1Yrh8M7MIvhLWjmv+pO9BPAKqscdrWjuw/zr/S9zSbIqsFmpp4i QYMZ3I/5QcELbhONIff4xZ4ubEe8cRjtgIkV0cl/t3hYsbUk8zmVaL8KUCtUakQq+4 dQjrUxHzST7Rj9+RJQnH4N1RK4N7gogLlJg8CHkFx7ICtfbhEIeLIs7onE0uTlgIcz UGJ/Yry2/mLDOXXffWWEPOUcUatlJo1eWzWgweTBGDzIET2eCeqFlZkBS8qbhlFG5/ YYjln4byJdDBg== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 6B90165646 for ; Fri, 6 May 2022 16:55:11 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="AOUYcxNP"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id D2557492; Fri, 6 May 2022 16:55:10 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651848911; bh=O45OCn0MzoZDSEqbqEq61DIKMlzSOl/dIVbUpTmGN3U=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=AOUYcxNPjkT6rYXOc+UFoCACGp21EIhnZdwRj1f0j2VAnogy6igzr6U6aokRPjM/x ZtX+U4WX4dUlxpi7Dioj8PgRinQNr6WKRP7F5Ubas4qoIryo5ZGPmHeg9fKEdY2oDJ g9nXYUVXYEdHhIQciFtAKk68+5T+aWEeu0JXhDnM= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Fri, 6 May 2022 17:54:10 +0300 Message-Id: <20220506145414.99039-4-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220506145414.99039-1-tomi.valkeinen@ideasonboard.com> References: <20220506145414.99039-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v8 3/7] meson: add 'check: true' for run_command() calls X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Add 'check: true' to all run_command() calls as suggested in https://github.com/mesonbuild/meson/issues/9300 to get rid of meson warning "You should add the boolean check kwarg to the run_command call." This makes meson fail if the executed command fails, which makes sense. Signed-off-by: Tomi Valkeinen Reviewed-by: Kieran Bingham Reviewed-by: Laurent Pinchart --- meson.build | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/meson.build b/meson.build index 10ad8c5c..0124e7d3 100644 --- a/meson.build +++ b/meson.build @@ -18,7 +18,8 @@ project('libcamera', 'c', 'cpp', # libcamera_git_version. libcamera_git_version = run_command('utils/gen-version.sh', meson.project_build_root(), - meson.project_source_root()).stdout().strip() + meson.project_source_root(), + check: true).stdout().strip() if libcamera_git_version == '' libcamera_git_version = meson.project_version() endif @@ -148,7 +149,7 @@ subdir('test') if not meson.is_cross_build() kernel_version_req = '>= 5.0.0' - kernel_version = run_command('uname', '-r').stdout().strip() + kernel_version = run_command('uname', '-r', check: true).stdout().strip() if not kernel_version.version_compare(kernel_version_req) warning('The current running kernel version @0@ is too old to run libcamera.' .format(kernel_version)) @@ -160,7 +161,8 @@ endif # Create a symlink from the build root to the source root. This is used when # running libcamera from the build directory to locate resources in the source # directory (such as IPA configuration files). -run_command('ln', '-fsT', meson.project_source_root(), meson.project_build_root() / 'source') +run_command('ln', '-fsT', meson.project_source_root(), meson.project_build_root() / 'source', + check: true) configure_file(output : 'config.h', configuration : config_h) From patchwork Fri May 6 14:54:11 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15815 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id CA50FC0F2A for ; Fri, 6 May 2022 14:55:17 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id A536D65658; Fri, 6 May 2022 16:55:15 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651848915; bh=X4fKcx0L0/W42h8JAMQN1oFzNGww07mmCcPqAcyAlD8=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=trwaGBM9kfDcHW9YNbvMfKimPKGT5q7utSbiyKo7WNaBV+esHP5F0/yE51jsSiJ/3 ERErCB8WbiMUl8sidCIyb+NYjnWc5CS6r9ZgIEqo6il7bRGwStXLpjC0CleqYaIKZK FCU+zc0NxLVCDo6vBNWN8xSWmpEKwPutI4oMfii/qSEv55qfMQwOAvdqhX/GjtgdVH r+fs03a1PiE4j8fpvlJWa7Koo0oXglkmbbz/tfnsOnjRsHf0qkRGo/POm5fcfialau xKX0sjoo3mk3E7QCHBLKLjYyVdhrSphpxfCxrC29RSTD28fCa/T+oZxW/XBrw8qKZQ AH9rdVFXFESrA== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 00FFA65646 for ; Fri, 6 May 2022 16:55:12 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="wHQnz5Ze"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 5E5D7835; Fri, 6 May 2022 16:55:11 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651848911; bh=X4fKcx0L0/W42h8JAMQN1oFzNGww07mmCcPqAcyAlD8=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=wHQnz5ZezKSEZohsFdiIHddSd5PgNRUkSmZF+LpbBhmh2ea1TkpzWqdj13idgzXjP 6FIcJYO7hf/hjFl68FD/D+IzVwK1u0ye3UzjrDXstDqLj+fiR2pFJ5otU8aNJ44R6N xG5WZznIjvZ5TBb2qTHVyiwxEx/KyzeVpYEOmR6I= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Fri, 6 May 2022 17:54:11 +0300 Message-Id: <20220506145414.99039-5-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220506145414.99039-1-tomi.valkeinen@ideasonboard.com> References: <20220506145414.99039-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v8 4/7] Add Python bindings X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Add libcamera Python bindings. pybind11 is used to generate the C++ <-> Python layer. We use pybind11 'smart_holder' version to avoid issues with private destructors and shared_ptr. There is also an alternative solution here: https://github.com/pybind/pybind11/pull/2067 Only a subset of libcamera classes are exposed. Implementing and testing the wrapper classes is challenging, and as such only classes that I have needed have been added so far. Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart --- meson.build | 1 + meson_options.txt | 5 + src/meson.build | 1 + src/py/libcamera/__init__.py | 84 +++ src/py/libcamera/meson.build | 51 ++ src/py/libcamera/pyenums.cpp | 34 + src/py/libcamera/pymain.cpp | 640 ++++++++++++++++++ src/py/meson.build | 1 + subprojects/.gitignore | 3 +- subprojects/packagefiles/pybind11/meson.build | 7 + subprojects/pybind11.wrap | 9 + 11 files changed, 835 insertions(+), 1 deletion(-) create mode 100644 src/py/libcamera/__init__.py create mode 100644 src/py/libcamera/meson.build create mode 100644 src/py/libcamera/pyenums.cpp create mode 100644 src/py/libcamera/pymain.cpp create mode 100644 src/py/meson.build create mode 100644 subprojects/packagefiles/pybind11/meson.build create mode 100644 subprojects/pybind11.wrap diff --git a/meson.build b/meson.build index 0124e7d3..60a911e0 100644 --- a/meson.build +++ b/meson.build @@ -177,6 +177,7 @@ summary({ 'Tracing support': tracing_enabled, 'Android support': android_enabled, 'GStreamer support': gst_enabled, + 'Python bindings': pycamera_enabled, 'V4L2 emulation support': v4l2_enabled, 'cam application': cam_enabled, 'qcam application': qcam_enabled, diff --git a/meson_options.txt b/meson_options.txt index 2c80ad8b..ca00c78e 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -58,3 +58,8 @@ option('v4l2', type : 'boolean', value : false, description : 'Compile the V4L2 compatibility layer') + +option('pycamera', + type : 'feature', + value : 'auto', + description : 'Enable libcamera Python bindings (experimental)') diff --git a/src/meson.build b/src/meson.build index e0ea9c35..34663a6f 100644 --- a/src/meson.build +++ b/src/meson.build @@ -37,4 +37,5 @@ subdir('cam') subdir('qcam') subdir('gstreamer') +subdir('py') subdir('v4l2') diff --git a/src/py/libcamera/__init__.py b/src/py/libcamera/__init__.py new file mode 100644 index 00000000..6b330890 --- /dev/null +++ b/src/py/libcamera/__init__.py @@ -0,0 +1,84 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# Copyright (C) 2022, Tomi Valkeinen + +from ._libcamera import * + + +class MappedFrameBuffer: + def __init__(self, fb): + self.__fb = fb + + def __enter__(self): + from os import lseek, SEEK_END + import mmap + + fb = self.__fb + + # Collect information about the buffers + + bufinfos = {} + + for i in range(fb.num_planes): + fd = fb.fd(i) + + if fd not in bufinfos: + buflen = lseek(fd, 0, SEEK_END) + bufinfos[fd] = {'maplen': 0, 'buflen': buflen} + else: + buflen = bufinfos[fd]['buflen'] + + if fb.offset(i) > buflen or fb.offset(i) + fb.length(i) > buflen: + raise RuntimeError(f'plane is out of buffer: buffer length={buflen}, ' + + f'plane offset={fb.offset(i)}, plane length={fb.length(i)}') + + bufinfos[fd]['maplen'] = max(bufinfos[fd]['maplen'], fb.offset(i) + fb.length(i)) + + # mmap the buffers + + maps = [] + + for fd, info in bufinfos.items(): + map = mmap.mmap(fd, info['maplen'], mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE) + info['map'] = map + maps.append(map) + + self.__maps = tuple(maps) + + # Create memoryviews for the planes + + planes = [] + + for i in range(fb.num_planes): + fd = fb.fd(i) + info = bufinfos[fd] + + mv = memoryview(info['map']) + + start = fb.offset(i) + end = fb.offset(i) + fb.length(i) + + mv = mv[start:end] + + planes.append(mv) + + self.__planes = tuple(planes) + + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + for p in self.__planes: + p.release() + + for mm in self.__maps: + mm.close() + + @property + def planes(self): + return self.__planes + + +def __FrameBuffer__mmap(self): + return MappedFrameBuffer(self) + + +FrameBuffer.mmap = __FrameBuffer__mmap diff --git a/src/py/libcamera/meson.build b/src/py/libcamera/meson.build new file mode 100644 index 00000000..e4abc34a --- /dev/null +++ b/src/py/libcamera/meson.build @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: CC0-1.0 + +py3_dep = dependency('python3', required : get_option('pycamera')) + +if not py3_dep.found() + pycamera_enabled = false + subdir_done() +endif + +pycamera_enabled = true + +pybind11_proj = subproject('pybind11') +pybind11_dep = pybind11_proj.get_variable('pybind11_dep') + +pycamera_sources = files([ + 'pyenums.cpp', + 'pymain.cpp', +]) + +pycamera_deps = [ + libcamera_public, + py3_dep, + pybind11_dep, +] + +pycamera_args = [ + '-fvisibility=hidden', + '-Wno-shadow', + '-DPYBIND11_USE_SMART_HOLDER_AS_DEFAULT', +] + +destdir = get_option('libdir') / ('python' + py3_dep.version()) / 'site-packages' / 'libcamera' + +pycamera = shared_module('_libcamera', + pycamera_sources, + install : true, + install_dir : destdir, + name_prefix : '', + dependencies : pycamera_deps, + cpp_args : pycamera_args) + +run_command('ln', '-fsT', '../../../../src/py/libcamera/__init__.py', + meson.current_build_dir() / '__init__.py', + check: true) + +install_data(['__init__.py'], install_dir : destdir) + +# \todo: Generate stubs when building. Depends on pybind11-stubgen. Sometimes +# this works, sometimes doesn't... To generate pylibcamera stubs. +# $ PYTHONPATH=build/src/py pybind11-stubgen --no-setup-py -o build/src/py libcamera +# $ mv build/src/py/libcamera-stubs/* build/src/py/libcamera/ diff --git a/src/py/libcamera/pyenums.cpp b/src/py/libcamera/pyenums.cpp new file mode 100644 index 00000000..b655e622 --- /dev/null +++ b/src/py/libcamera/pyenums.cpp @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2022, Tomi Valkeinen + * + * Python bindings - Enumerations + */ + +#include + +#include + +namespace py = pybind11; + +using namespace libcamera; + +void init_pyenums(py::module &m) +{ + py::enum_(m, "StreamRole") + .value("StillCapture", StreamRole::StillCapture) + .value("Raw", StreamRole::Raw) + .value("VideoRecording", StreamRole::VideoRecording) + .value("Viewfinder", StreamRole::Viewfinder); + + py::enum_(m, "ControlType") + .value("None", ControlType::ControlTypeNone) + .value("Bool", ControlType::ControlTypeBool) + .value("Byte", ControlType::ControlTypeByte) + .value("Integer32", ControlType::ControlTypeInteger32) + .value("Integer64", ControlType::ControlTypeInteger64) + .value("Float", ControlType::ControlTypeFloat) + .value("String", ControlType::ControlTypeString) + .value("Rectangle", ControlType::ControlTypeRectangle) + .value("Size", ControlType::ControlTypeSize); +} diff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp new file mode 100644 index 00000000..8c3be8f4 --- /dev/null +++ b/src/py/libcamera/pymain.cpp @@ -0,0 +1,640 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2022, Tomi Valkeinen + * + * Python bindings + */ + +/* + * \todo Add geometry classes (Point, Rectangle...) + * \todo Add bindings for the ControlInfo class + */ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace py = pybind11; + +using namespace libcamera; + +template +static py::object valueOrTuple(const ControlValue &cv) +{ + if (cv.isArray()) { + const T *v = reinterpret_cast(cv.data().data()); + auto t = py::tuple(cv.numElements()); + + for (size_t i = 0; i < cv.numElements(); ++i) + t[i] = v[i]; + + return t; + } + + return py::cast(cv.get()); +} + +static py::object controlValueToPy(const ControlValue &cv) +{ + switch (cv.type()) { + case ControlTypeBool: + return valueOrTuple(cv); + case ControlTypeByte: + return valueOrTuple(cv); + case ControlTypeInteger32: + return valueOrTuple(cv); + case ControlTypeInteger64: + return valueOrTuple(cv); + case ControlTypeFloat: + return valueOrTuple(cv); + case ControlTypeString: + return py::cast(cv.get()); + case ControlTypeRectangle: { + const Rectangle *v = reinterpret_cast(cv.data().data()); + return py::make_tuple(v->x, v->y, v->width, v->height); + } + case ControlTypeSize: { + const Size *v = reinterpret_cast(cv.data().data()); + return py::make_tuple(v->width, v->height); + } + case ControlTypeNone: + default: + throw std::runtime_error("Unsupported ControlValue type"); + } +} + +template +static ControlValue controlValueMaybeArray(const py::object &ob) +{ + if (py::isinstance(ob) || py::isinstance(ob)) { + std::vector vec = ob.cast>(); + return ControlValue(Span(vec)); + } + + return ControlValue(ob.cast()); +} + +static ControlValue pyToControlValue(const py::object &ob, ControlType type) +{ + switch (type) { + case ControlTypeBool: + return ControlValue(ob.cast()); + case ControlTypeByte: + return controlValueMaybeArray(ob); + case ControlTypeInteger32: + return controlValueMaybeArray(ob); + case ControlTypeInteger64: + return controlValueMaybeArray(ob); + case ControlTypeFloat: + return controlValueMaybeArray(ob); + case ControlTypeString: + return ControlValue(ob.cast()); + case ControlTypeRectangle: { + auto array = ob.cast>(); + return ControlValue(Rectangle(array[0], array[1], array[2], array[3])); + } + case ControlTypeSize: { + auto array = ob.cast>(); + return ControlValue(Size(array[0], array[1])); + } + case ControlTypeNone: + default: + throw std::runtime_error("Control type not implemented"); + } +} + +static std::weak_ptr gCameraManager; +static int gEventfd; +static std::mutex gReqlistMutex; +static std::vector gReqList; + +static void handleRequestCompleted(Request *req) +{ + { + std::lock_guard guard(gReqlistMutex); + gReqList.push_back(req); + } + + uint64_t v = 1; + write(gEventfd, &v, 8); +} + +void init_pyenums(py::module &m); + +PYBIND11_MODULE(_libcamera, m) +{ + init_pyenums(m); + + /* Forward declarations */ + + /* + * We need to declare all the classes here so that Python docstrings + * can be generated correctly. + * https://pybind11.readthedocs.io/en/latest/advanced/misc.html#avoiding-c-types-in-docstrings + */ + + auto pyCameraManager = py::class_(m, "CameraManager"); + auto pyCamera = py::class_(m, "Camera"); + auto pyCameraConfiguration = py::class_(m, "CameraConfiguration"); + auto pyCameraConfigurationStatus = py::enum_(pyCameraConfiguration, "Status"); + auto pyStreamConfiguration = py::class_(m, "StreamConfiguration"); + auto pyStreamFormats = py::class_(m, "StreamFormats"); + auto pyFrameBufferAllocator = py::class_(m, "FrameBufferAllocator"); + auto pyFrameBuffer = py::class_(m, "FrameBuffer"); + auto pyStream = py::class_(m, "Stream"); + auto pyControlId = py::class_(m, "ControlId"); + auto pyRequest = py::class_(m, "Request"); + auto pyRequestStatus = py::enum_(pyRequest, "Status"); + auto pyRequestReuse = py::enum_(pyRequest, "Reuse"); + auto pyFrameMetadata = py::class_(m, "FrameMetadata"); + auto pyFrameMetadataStatus = py::enum_(pyFrameMetadata, "Status"); + auto pyTransform = py::class_(m, "Transform"); + auto pyColorSpace = py::class_(m, "ColorSpace"); + auto pyColorSpacePrimaries = py::enum_(pyColorSpace, "Primaries"); + auto pyColorSpaceTransferFunction = py::enum_(pyColorSpace, "TransferFunction"); + auto pyColorSpaceYcbcrEncoding = py::enum_(pyColorSpace, "YcbcrEncoding"); + auto pyColorSpaceRange = py::enum_(pyColorSpace, "Range"); + + /* Global functions */ + m.def("log_set_level", &logSetLevel); + + /* Classes */ + pyCameraManager + .def_static("singleton", []() { + std::shared_ptr cm = gCameraManager.lock(); + if (cm) + return cm; + + int fd = eventfd(0, 0); + if (fd == -1) + throw std::system_error(errno, std::generic_category(), + "Failed to create eventfd"); + + cm = std::shared_ptr(new CameraManager, [](auto p) { + close(gEventfd); + gEventfd = -1; + delete p; + }); + + gEventfd = fd; + gCameraManager = cm; + + int ret = cm->start(); + if (ret) + throw std::system_error(-ret, std::generic_category(), + "Failed to start CameraManager"); + + return cm; + }) + + .def_property_readonly("version", &CameraManager::version) + + .def_property_readonly("efd", [](CameraManager &) { + return gEventfd; + }) + + .def("get_ready_requests", [](CameraManager &) { + std::vector v; + + { + std::lock_guard guard(gReqlistMutex); + swap(v, gReqList); + } + + std::vector ret; + + for (Request *req : v) { + py::object o = py::cast(req); + /* Decrease the ref increased in Camera.queue_request() */ + o.dec_ref(); + ret.push_back(o); + } + + return ret; + }) + + .def("get", py::overload_cast(&CameraManager::get), py::keep_alive<0, 1>()) + + /* Create a list of Cameras, where each camera has a keep-alive to CameraManager */ + .def_property_readonly("cameras", [](CameraManager &self) { + py::list l; + + for (auto &c : self.cameras()) { + py::object py_cm = py::cast(self); + py::object py_cam = py::cast(c); + py::detail::keep_alive_impl(py_cam, py_cm); + l.append(py_cam); + } + + return l; + }); + + pyCamera + .def_property_readonly("id", &Camera::id) + .def("acquire", &Camera::acquire) + .def("release", &Camera::release) + .def("start", [](Camera &self, py::dict controls) { + /* \todo What happens if someone calls start() multiple times? */ + + self.requestCompleted.connect(handleRequestCompleted); + + const ControlInfoMap &controlMap = self.controls(); + ControlList controlList(controlMap); + for (const auto& [hkey, hval]: controls) { + auto key = hkey.cast(); + + auto it = find_if(controlMap.begin(), controlMap.end(), + [&key](const auto &kvp) { + return kvp.first->name() == key; }); + + if (it == controlMap.end()) + throw std::runtime_error("Control " + key + " not found"); + + const auto &id = it->first; + auto obj = py::cast(hval); + + controlList.set(id->id(), pyToControlValue(obj, id->type())); + } + + int ret = self.start(&controlList); + if (ret) { + self.requestCompleted.disconnect(handleRequestCompleted); + return ret; + } + + return 0; + }, py::arg("controls") = py::dict()) + + .def("stop", [](Camera &self) { + int ret = self.stop(); + if (ret) + return ret; + + self.requestCompleted.disconnect(handleRequestCompleted); + + return 0; + }) + + .def("__repr__", [](Camera &self) { + return ""; + }) + + /* Keep the camera alive, as StreamConfiguration contains a Stream* */ + .def("generate_configuration", &Camera::generateConfiguration, py::keep_alive<0, 1>()) + .def("configure", &Camera::configure) + + .def("create_request", &Camera::createRequest, py::arg("cookie") = 0) + + .def("queue_request", [](Camera &self, Request *req) { + py::object py_req = py::cast(req); + + /* + * Increase the reference count, will be dropped in + * CameraManager.get_ready_requests(). + */ + + py_req.inc_ref(); + + int ret = self.queueRequest(req); + if (ret) + py_req.dec_ref(); + + return ret; + }) + + .def_property_readonly("streams", [](Camera &self) { + py::set set; + for (auto &s : self.streams()) { + py::object py_self = py::cast(self); + py::object py_s = py::cast(s); + py::detail::keep_alive_impl(py_s, py_self); + set.add(py_s); + } + return set; + }) + + .def("find_control", [](Camera &self, const std::string &name) { + const auto &controls = self.controls(); + + auto it = find_if(controls.begin(), controls.end(), + [&name](const auto &kvp) { return kvp.first->name() == name; }); + + if (it == controls.end()) + throw std::runtime_error("Control not found"); + + return it->first; + }, py::return_value_policy::reference_internal) + + .def_property_readonly("controls", [](Camera &self) { + py::dict ret; + + for (const auto &[id, ci] : self.controls()) { + ret[id->name().c_str()] = std::make_tuple(controlValueToPy(ci.min()), + controlValueToPy(ci.max()), + controlValueToPy(ci.def())); + } + + return ret; + }) + + .def_property_readonly("properties", [](Camera &self) { + py::dict ret; + + for (const auto &[key, cv] : self.properties()) { + const ControlId *id = properties::properties.at(key); + py::object ob = controlValueToPy(cv); + + ret[id->name().c_str()] = ob; + } + + return ret; + }); + + pyCameraConfiguration + .def("__iter__", [](CameraConfiguration &self) { + return py::make_iterator(self); + }, py::keep_alive<0, 1>()) + .def("__len__", [](CameraConfiguration &self) { + return self.size(); + }) + .def("validate", &CameraConfiguration::validate) + .def("at", py::overload_cast(&CameraConfiguration::at), + py::return_value_policy::reference_internal) + .def_property_readonly("size", &CameraConfiguration::size) + .def_property_readonly("empty", &CameraConfiguration::empty) + .def_readwrite("transform", &CameraConfiguration::transform); + + pyCameraConfigurationStatus + .value("Valid", CameraConfiguration::Valid) + .value("Adjusted", CameraConfiguration::Adjusted) + .value("Invalid", CameraConfiguration::Invalid); + + pyStreamConfiguration + .def("to_string", &StreamConfiguration::toString) + .def_property_readonly("stream", &StreamConfiguration::stream, + py::return_value_policy::reference_internal) + .def_property( + "size", + [](StreamConfiguration &self) { + return std::make_tuple(self.size.width, self.size.height); + }, + [](StreamConfiguration &self, std::tuple size) { + self.size.width = std::get<0>(size); + self.size.height = std::get<1>(size); + }) + .def_property( + "pixel_format", + [](StreamConfiguration &self) { + return self.pixelFormat.toString(); + }, + [](StreamConfiguration &self, std::string fmt) { + self.pixelFormat = PixelFormat::fromString(fmt); + }) + .def_readwrite("stride", &StreamConfiguration::stride) + .def_readwrite("frame_size", &StreamConfiguration::frameSize) + .def_readwrite("buffer_count", &StreamConfiguration::bufferCount) + .def_property_readonly("formats", &StreamConfiguration::formats, + py::return_value_policy::reference_internal) + .def_readwrite("colorSpace", &StreamConfiguration::colorSpace); + + pyStreamFormats + .def_property_readonly("pixel_formats", [](StreamFormats &self) { + std::vector fmts; + for (auto &fmt : self.pixelformats()) + fmts.push_back(fmt.toString()); + return fmts; + }) + .def("sizes", [](StreamFormats &self, const std::string &pixelFormat) { + auto fmt = PixelFormat::fromString(pixelFormat); + std::vector> fmts; + for (const auto &s : self.sizes(fmt)) + fmts.push_back(std::make_tuple(s.width, s.height)); + return fmts; + }) + .def("range", [](StreamFormats &self, const std::string &pixelFormat) { + auto fmt = PixelFormat::fromString(pixelFormat); + const auto &range = self.range(fmt); + return make_tuple(std::make_tuple(range.hStep, range.vStep), + std::make_tuple(range.min.width, range.min.height), + std::make_tuple(range.max.width, range.max.height)); + }); + + pyFrameBufferAllocator + .def(py::init>(), py::keep_alive<1, 2>()) + .def("allocate", &FrameBufferAllocator::allocate) + .def_property_readonly("allocated", &FrameBufferAllocator::allocated) + /* Create a list of FrameBuffers, where each FrameBuffer has a keep-alive to FrameBufferAllocator */ + .def("buffers", [](FrameBufferAllocator &self, Stream *stream) { + py::object py_self = py::cast(self); + py::list l; + for (auto &ub : self.buffers(stream)) { + py::object py_buf = py::cast(ub.get(), py::return_value_policy::reference_internal, py_self); + l.append(py_buf); + } + return l; + }); + + pyFrameBuffer + /* \todo implement FrameBuffer::Plane properly */ + .def(py::init([](std::vector> planes, unsigned int cookie) { + std::vector v; + for (const auto &t : planes) + v.push_back({ SharedFD(std::get<0>(t)), FrameBuffer::Plane::kInvalidOffset, std::get<1>(t) }); + return new FrameBuffer(v, cookie); + })) + .def_property_readonly("metadata", &FrameBuffer::metadata, py::return_value_policy::reference_internal) + .def_property_readonly("num_planes", [](const FrameBuffer &self) { + return self.planes().size(); + }) + .def("length", [](FrameBuffer &self, uint32_t idx) { + const FrameBuffer::Plane &plane = self.planes()[idx]; + return plane.length; + }) + .def("fd", [](FrameBuffer &self, uint32_t idx) { + const FrameBuffer::Plane &plane = self.planes()[idx]; + return plane.fd.get(); + }) + .def("offset", [](FrameBuffer &self, uint32_t idx) { + const FrameBuffer::Plane &plane = self.planes()[idx]; + return plane.offset; + }) + .def_property("cookie", &FrameBuffer::cookie, &FrameBuffer::setCookie); + + pyStream + .def_property_readonly("configuration", &Stream::configuration); + + pyControlId + .def_property_readonly("id", &ControlId::id) + .def_property_readonly("name", &ControlId::name) + .def_property_readonly("type", &ControlId::type); + + pyRequest + /* \todo Fence is not supported, so we cannot expose addBuffer() directly */ + .def("add_buffer", [](Request &self, const Stream *stream, FrameBuffer *buffer) { + return self.addBuffer(stream, buffer); + }, py::keep_alive<1, 3>()) /* Request keeps Framebuffer alive */ + .def_property_readonly("status", &Request::status) + .def_property_readonly("buffers", &Request::buffers) + .def_property_readonly("cookie", &Request::cookie) + .def_property_readonly("has_pending_buffers", &Request::hasPendingBuffers) + .def("set_control", [](Request &self, ControlId &id, py::object value) { + self.controls().set(id.id(), pyToControlValue(value, id.type())); + }) + .def_property_readonly("metadata", [](Request &self) { + py::dict ret; + + for (const auto &[key, cv] : self.metadata()) { + const ControlId *id = controls::controls.at(key); + py::object ob = controlValueToPy(cv); + + ret[id->name().c_str()] = ob; + } + + return ret; + }) + /* + * \todo As we add a keep_alive to the fb in addBuffers(), we + * can only allow reuse with ReuseBuffers. + */ + .def("reuse", [](Request &self) { self.reuse(Request::ReuseFlag::ReuseBuffers); }); + + pyRequestStatus + .value("Pending", Request::RequestPending) + .value("Complete", Request::RequestComplete) + .value("Cancelled", Request::RequestCancelled); + + pyRequestReuse + .value("Default", Request::ReuseFlag::Default) + .value("ReuseBuffers", Request::ReuseFlag::ReuseBuffers); + + pyFrameMetadata + .def_readonly("status", &FrameMetadata::status) + .def_readonly("sequence", &FrameMetadata::sequence) + .def_readonly("timestamp", &FrameMetadata::timestamp) + /* \todo Implement FrameMetadata::Plane properly */ + .def_property_readonly("bytesused", [](FrameMetadata &self) { + std::vector v; + v.resize(self.planes().size()); + transform(self.planes().begin(), self.planes().end(), v.begin(), [](const auto &p) { return p.bytesused; }); + return v; + }); + + pyFrameMetadataStatus + .value("Success", FrameMetadata::FrameSuccess) + .value("Error", FrameMetadata::FrameError) + .value("Cancelled", FrameMetadata::FrameCancelled); + + pyTransform + .def(py::init([](int rotation, bool hflip, bool vflip, bool transpose) { + bool ok; + + Transform t = transformFromRotation(rotation, &ok); + if (!ok) + throw std::invalid_argument("Invalid rotation"); + + if (hflip) + t ^= Transform::HFlip; + if (vflip) + t ^= Transform::VFlip; + if (transpose) + t ^= Transform::Transpose; + return t; + }), py::arg("rotation") = 0, py::arg("hflip") = false, + py::arg("vflip") = false, py::arg("transpose") = false) + .def(py::init([](Transform &other) { return other; })) + .def("__repr__", [](Transform &self) { + return ""; + }) + .def_property("hflip", + [](Transform &self) { + return !!(self & Transform::HFlip); + }, + [](Transform &self, bool hflip) { + if (hflip) + self |= Transform::HFlip; + else + self &= ~Transform::HFlip; + }) + .def_property("vflip", + [](Transform &self) { + return !!(self & Transform::VFlip); + }, + [](Transform &self, bool vflip) { + if (vflip) + self |= Transform::VFlip; + else + self &= ~Transform::VFlip; + }) + .def_property("transpose", + [](Transform &self) { + return !!(self & Transform::Transpose); + }, + [](Transform &self, bool transpose) { + if (transpose) + self |= Transform::Transpose; + else + self &= ~Transform::Transpose; + }) + .def("inverse", [](Transform &self) { return -self; }) + .def("invert", [](Transform &self) { + self = -self; + }) + .def("compose", [](Transform &self, Transform &other) { + self = self * other; + }); + + pyColorSpace + .def(py::init([](ColorSpace::Primaries primaries, + ColorSpace::TransferFunction transferFunction, + ColorSpace::YcbcrEncoding ycbcrEncoding, + ColorSpace::Range range) { + return ColorSpace(primaries, transferFunction, ycbcrEncoding, range); + }), py::arg("primaries"), py::arg("transferFunction"), + py::arg("ycbcrEncoding"), py::arg("range")) + .def(py::init([](ColorSpace &other) { return other; })) + .def("__repr__", [](ColorSpace &self) { + return ""; + }) + .def_readwrite("primaries", &ColorSpace::primaries) + .def_readwrite("transferFunction", &ColorSpace::transferFunction) + .def_readwrite("ycbcrEncoding", &ColorSpace::ycbcrEncoding) + .def_readwrite("range", &ColorSpace::range) + .def_static("Raw", []() { return ColorSpace::Raw; }) + .def_static("Jpeg", []() { return ColorSpace::Jpeg; }) + .def_static("Srgb", []() { return ColorSpace::Srgb; }) + .def_static("Smpte170m", []() { return ColorSpace::Smpte170m; }) + .def_static("Rec709", []() { return ColorSpace::Rec709; }) + .def_static("Rec2020", []() { return ColorSpace::Rec2020; }); + + pyColorSpacePrimaries + .value("Raw", ColorSpace::Primaries::Raw) + .value("Smpte170m", ColorSpace::Primaries::Smpte170m) + .value("Rec709", ColorSpace::Primaries::Rec709) + .value("Rec2020", ColorSpace::Primaries::Rec2020); + + pyColorSpaceTransferFunction + .value("Linear", ColorSpace::TransferFunction::Linear) + .value("Srgb", ColorSpace::TransferFunction::Srgb) + .value("Rec709", ColorSpace::TransferFunction::Rec709); + + pyColorSpaceYcbcrEncoding + .value("Null", ColorSpace::YcbcrEncoding::None) + .value("Rec601", ColorSpace::YcbcrEncoding::Rec601) + .value("Rec709", ColorSpace::YcbcrEncoding::Rec709) + .value("Rec2020", ColorSpace::YcbcrEncoding::Rec2020); + + pyColorSpaceRange + .value("Full", ColorSpace::Range::Full) + .value("Limited", ColorSpace::Range::Limited); +} diff --git a/src/py/meson.build b/src/py/meson.build new file mode 100644 index 00000000..4ce9668c --- /dev/null +++ b/src/py/meson.build @@ -0,0 +1 @@ +subdir('libcamera') diff --git a/subprojects/.gitignore b/subprojects/.gitignore index 391fde2c..0e194289 100644 --- a/subprojects/.gitignore +++ b/subprojects/.gitignore @@ -1,3 +1,4 @@ /googletest-release* /libyuv -/packagecache \ No newline at end of file +/packagecache +/pybind11 diff --git a/subprojects/packagefiles/pybind11/meson.build b/subprojects/packagefiles/pybind11/meson.build new file mode 100644 index 00000000..1be47ca4 --- /dev/null +++ b/subprojects/packagefiles/pybind11/meson.build @@ -0,0 +1,7 @@ +project('pybind11', 'cpp', + version : '2.9.1', + license : 'BSD-3-Clause') + +pybind11_incdir = include_directories('include') + +pybind11_dep = declare_dependency(include_directories : pybind11_incdir) diff --git a/subprojects/pybind11.wrap b/subprojects/pybind11.wrap new file mode 100644 index 00000000..43c0608d --- /dev/null +++ b/subprojects/pybind11.wrap @@ -0,0 +1,9 @@ +[wrap-git] +url = https://github.com/pybind/pybind11.git +# This is the head of 'smart_holder' branch +revision = 82734801f23314b4c34d70a79509e060a2648e04 +depth = 1 +patch_directory = pybind11 + +[provide] +pybind11 = pybind11_dep From patchwork Fri May 6 14:54:12 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15816 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 6792AC326D for ; Fri, 6 May 2022 14:55:18 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 162B46564E; Fri, 6 May 2022 16:55:17 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651848917; bh=DvgvXWPQWbX4CVyUuHozOyb5aagpwZHR4TxH96i+zlM=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=MijnuOqVDieHLv2JkfNoGyLf56FrjiIuLHnLdVrdmn5gZZeo3iHggYZwVvRD2CQ82 H4ojju2MzR256hhjjkazL67Wj1guQDwQfFMc6640JrAtnREoQa2lHqd/9ql6rAaxSk v/5XOxTBNLaHXcmlWgkEuKel0bEYWxPt9G1chV9GmS+LVToAlh3r1huWi6hFlw9cd5 Vcq+lbO53eyERiB8NxLNuqrx5NAu1L3/p0tiI14Kp+rYjuFcylWLJaAfpOxiKuZNm3 qSUQtTEc2LETKKDn0SgqZkbYJdR/1B2PghwZeTVOTkVFxaLuK7JEfYRLN4+CyiunQu IjrVl1xsckP6Q== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 9A49E6564C for ; Fri, 6 May 2022 16:55:12 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="vqxMTxUQ"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id EEE86A43; Fri, 6 May 2022 16:55:11 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651848912; bh=DvgvXWPQWbX4CVyUuHozOyb5aagpwZHR4TxH96i+zlM=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=vqxMTxUQ3qM4hcK9ebbea/j5uxXsSUhx5pwW0yqDnP1AA7OCgH3MThhmEB5j2afXZ Cecxzwj2JKZEWOTfDilt6Vqqilo/j+ucZa3ayWz1IqT7Ul7vxl1niJ7ssk/hQxwUEf sZGN1RuQX0x1I+JvZYCVa0XmoU147DqCmHmS6cec= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Fri, 6 May 2022 17:54:12 +0300 Message-Id: <20220506145414.99039-6-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220506145414.99039-1-tomi.valkeinen@ideasonboard.com> References: <20220506145414.99039-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v8 5/7] py: generate control enums from yaml X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Generate enums for controls from control_ids.yaml. The generator script has some heuristics to generate nicer enum names. E.g. instead of having "LensShadingMapMode.LensShadingMapModeOff" we get "LensShadingMapMode.Off". This heuristics may need to be updated when the yaml file is changed or new controls are added. Signed-off-by: Tomi Valkeinen Reviewed-by: Kieran Bingham Reviewed-by: Laurent Pinchart --- src/py/libcamera/gen-py-control-enums.py | 95 +++++++++++++++++++++++ src/py/libcamera/meson.build | 12 +++ src/py/libcamera/pyenums_generated.cpp.in | 21 +++++ src/py/libcamera/pymain.cpp | 2 + 4 files changed, 130 insertions(+) create mode 100755 src/py/libcamera/gen-py-control-enums.py create mode 100644 src/py/libcamera/pyenums_generated.cpp.in diff --git a/src/py/libcamera/gen-py-control-enums.py b/src/py/libcamera/gen-py-control-enums.py new file mode 100755 index 00000000..dcc28b1a --- /dev/null +++ b/src/py/libcamera/gen-py-control-enums.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Generate Python bindings enums for controls from YAML + +import argparse +import string +import sys +import yaml + + +def find_common_prefix(strings): + prefix = strings[0] + + for string in strings[1:]: + while string[:len(prefix)] != prefix and prefix: + prefix = prefix[:len(prefix) - 1] + if not prefix: + break + + return prefix + + +def generate_py(controls): + out = '' + + for ctrl in controls: + name, ctrl = ctrl.popitem() + + enum = ctrl.get('enum') + if not enum: + continue + + if ctrl.get('draft'): + ns = 'libcamera::controls::draft::' + else: + ns = 'libcamera::controls::' + + cpp_enum = name + 'Enum' + + out += '\tpy::enum_<{}{}>(m, \"{}\")\n'.format(ns, cpp_enum, name) + + if name == 'LensShadingMapMode': + prefix = 'LensShadingMapMode' + else: + prefix = find_common_prefix([e['name'] for e in enum]) + + for entry in enum: + cpp_enum = entry['name'] + py_enum = entry['name'][len(prefix):] + + out += '\t\t.value(\"{}\", {}{})\n'.format(py_enum, ns, cpp_enum) + + out += '\t;\n' + + return {'enums': out} + + +def fill_template(template, data): + template = open(template, 'rb').read() + template = template.decode('utf-8') + template = string.Template(template) + return template.substitute(data) + + +def main(argv): + # Parse command line arguments + parser = argparse.ArgumentParser() + parser.add_argument('-o', dest='output', metavar='file', type=str, + help='Output file name. Defaults to standard output if not specified.') + parser.add_argument('input', type=str, + help='Input file name.') + parser.add_argument('template', type=str, + help='Template file name.') + args = parser.parse_args(argv[1:]) + + data = open(args.input, 'rb').read() + controls = yaml.safe_load(data)['controls'] + + data = generate_py(controls) + + data = fill_template(args.template, data) + + if args.output: + output = open(args.output, 'wb') + output.write(data.encode('utf-8')) + output.close() + else: + sys.stdout.write(data) + + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/src/py/libcamera/meson.build b/src/py/libcamera/meson.build index e4abc34a..f4c7590c 100644 --- a/src/py/libcamera/meson.build +++ b/src/py/libcamera/meson.build @@ -17,6 +17,18 @@ pycamera_sources = files([ 'pymain.cpp', ]) +gen_input_files = [ + meson.project_source_root() / 'src' / 'libcamera' / 'control_ids.yaml', + 'pyenums_generated.cpp.in' +] + +generated_sources = custom_target('py_gen_controls', + input : gen_input_files, + output : ['pyenums_generated.cpp'], + command : ['gen-py-control-enums.py', '-o', '@OUTPUT@', '@INPUT@']) + +pycamera_sources += generated_sources + pycamera_deps = [ libcamera_public, py3_dep, diff --git a/src/py/libcamera/pyenums_generated.cpp.in b/src/py/libcamera/pyenums_generated.cpp.in new file mode 100644 index 00000000..96daf257 --- /dev/null +++ b/src/py/libcamera/pyenums_generated.cpp.in @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2021, Tomi Valkeinen + * + * Python bindings + * + * This file is auto-generated. Do not edit. + */ + +#include + +#include + +namespace py = pybind11; + +using namespace libcamera; + +void init_pyenums_generated(py::module& m) +{ +${enums} +} diff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp index 8c3be8f4..aff672aa 100644 --- a/src/py/libcamera/pymain.cpp +++ b/src/py/libcamera/pymain.cpp @@ -131,10 +131,12 @@ static void handleRequestCompleted(Request *req) } void init_pyenums(py::module &m); +void init_pyenums_generated(py::module &m); PYBIND11_MODULE(_libcamera, m) { init_pyenums(m); + init_pyenums_generated(m); /* Forward declarations */ From patchwork Fri May 6 14:54:13 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15817 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 1327EC326C for ; Fri, 6 May 2022 14:55:19 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 72D4065660; Fri, 6 May 2022 16:55:18 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651848918; bh=5dvrmpnIxMrEeM2EX3VJMkMAhM0T2w8XNKQDdlJj7jg=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=xVLeEuLXCVv1fBLVrc3mwXV2nOTPTZehh6uUjPpEMFzkUS+9JE6YVfE+REHjjXuDh AYVicVTJh0Yzd0Ch0HfTJchTZyN/OaXn6DoJO8lS2SZWvE4RUX3Nn3JckFHW5FPKbe TEjAvPI6W1163ItofTwOK6YpAC9MVOexWA56LbarL69Nt4RMbQLtca1dRC2SToEdf/ 3DxhTNC5ccUPsSMfp6YiVOrlb1GVJmjS4i+iKgF0tOZjL4lOrL7A/LS8uzxxjtNmMr nNEATMsJZ6WtG/Qi31sTmogFnzKmH50us5u4KcNH5c2DSOj29/kQP1QYSF03+FXDe3 RxZMWLhpjI+Rg== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 1C5E76564E for ; Fri, 6 May 2022 16:55:13 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="lIWHbj+4"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 81EAE492; Fri, 6 May 2022 16:55:12 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651848912; bh=5dvrmpnIxMrEeM2EX3VJMkMAhM0T2w8XNKQDdlJj7jg=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=lIWHbj+4uX0hskeuG1C1WfsZBlAWyLJbdfHGORV77lyhIG9OpQxFFgo27vCt2OJRb Fi+BDrTnqRs/R2JmVhGVvmqQJq2L1zJvq/sF1KLjcoIW1E8aKUGHODEoIPrZfuIWB2 YNxvX2PViJ93Onu0Z85BXdqrLTrhEw9DRckYGZps= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Fri, 6 May 2022 17:54:13 +0300 Message-Id: <20220506145414.99039-7-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220506145414.99039-1-tomi.valkeinen@ideasonboard.com> References: <20220506145414.99039-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v8 6/7] py: add unittests.py X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Add a simple unittests.py as a base for python unittests. Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart --- test/meson.build | 1 + test/py/meson.build | 17 +++ test/py/unittests.py | 352 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 370 insertions(+) create mode 100644 test/py/meson.build create mode 100755 test/py/unittests.py diff --git a/test/meson.build b/test/meson.build index fd4c5ca0..623f3baa 100644 --- a/test/meson.build +++ b/test/meson.build @@ -18,6 +18,7 @@ subdir('log') subdir('media_device') subdir('pipeline') subdir('process') +subdir('py') subdir('serialization') subdir('stream') subdir('v4l2_compat') diff --git a/test/py/meson.build b/test/py/meson.build new file mode 100644 index 00000000..2affdbd4 --- /dev/null +++ b/test/py/meson.build @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: CC0-1.0 + +if not pycamera_enabled + subdir_done() +endif + +pymod = import('python') +py3 = pymod.find_installation('python3') + +pypathdir = meson.project_build_root() / 'src' / 'py' + +test('pyunittests', + py3, + args : files('unittests.py'), + env : ['PYTHONPATH=' + pypathdir], + suite : 'pybindings', + is_parallel : false) diff --git a/test/py/unittests.py b/test/py/unittests.py new file mode 100755 index 00000000..cbc00ff3 --- /dev/null +++ b/test/py/unittests.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2022, Tomi Valkeinen + +from collections import defaultdict +import errno +import gc +import libcamera as libcam +import os +import selectors +import time +import unittest +import weakref + + +class BaseTestCase(unittest.TestCase): + def assertZero(self, a, msg=None): + self.assertEqual(a, 0, msg) + + +class SimpleTestMethods(BaseTestCase): + def test_get_ref(self): + cm = libcam.CameraManager.singleton() + wr_cm = weakref.ref(cm) + + cam = cm.get('platform/vimc.0 Sensor B') + self.assertIsNotNone(cam) + wr_cam = weakref.ref(cam) + + cm = None + gc.collect() + self.assertIsNotNone(wr_cm()) + + cam = None + gc.collect() + self.assertIsNone(wr_cm()) + self.assertIsNone(wr_cam()) + + def test_acquire_release(self): + cm = libcam.CameraManager.singleton() + cam = cm.get('platform/vimc.0 Sensor B') + self.assertIsNotNone(cam) + + ret = cam.acquire() + self.assertZero(ret) + + ret = cam.release() + self.assertZero(ret) + + def test_double_acquire(self): + cm = libcam.CameraManager.singleton() + cam = cm.get('platform/vimc.0 Sensor B') + self.assertIsNotNone(cam) + + ret = cam.acquire() + self.assertZero(ret) + + libcam.log_set_level('Camera', 'FATAL') + ret = cam.acquire() + self.assertEqual(ret, -errno.EBUSY) + libcam.log_set_level('Camera', 'ERROR') + + ret = cam.release() + self.assertZero(ret) + + ret = cam.release() + # I expected EBUSY, but looks like double release works fine + self.assertZero(ret) + + +class CameraTesterBase(BaseTestCase): + def setUp(self): + self.cm = libcam.CameraManager.singleton() + self.cam = next((cam for cam in self.cm.cameras if 'platform/vimc' in cam.id), None) + if self.cam is None: + self.cm = None + self.skipTest('No vimc found') + + ret = self.cam.acquire() + if ret != 0: + self.cam = None + self.cm = None + raise Exception('Failed to acquire camera') + + def tearDown(self): + # If a test fails, the camera may be in running state. So always stop. + self.cam.stop() + + ret = self.cam.release() + if ret != 0: + raise Exception('Failed to release camera') + + self.cam = None + self.cm = None + + +class AllocatorTestMethods(CameraTesterBase): + def test_allocator(self): + cam = self.cam + + camconfig = cam.generate_configuration([libcam.StreamRole.StillCapture]) + self.assertTrue(camconfig.size == 1) + wr_camconfig = weakref.ref(camconfig) + + streamconfig = camconfig.at(0) + wr_streamconfig = weakref.ref(streamconfig) + + ret = cam.configure(camconfig) + self.assertZero(ret) + + stream = streamconfig.stream + wr_stream = weakref.ref(stream) + + # stream should keep streamconfig and camconfig alive + streamconfig = None + camconfig = None + gc.collect() + self.assertIsNotNone(wr_camconfig()) + self.assertIsNotNone(wr_streamconfig()) + + allocator = libcam.FrameBufferAllocator(cam) + ret = allocator.allocate(stream) + self.assertTrue(ret > 0) + wr_allocator = weakref.ref(allocator) + + buffers = allocator.buffers(stream) + buffers = None + + buffer = allocator.buffers(stream)[0] + self.assertIsNotNone(buffer) + wr_buffer = weakref.ref(buffer) + + allocator = None + gc.collect() + self.assertIsNotNone(wr_buffer()) + self.assertIsNotNone(wr_allocator()) + self.assertIsNotNone(wr_stream()) + + buffer = None + gc.collect() + self.assertIsNone(wr_buffer()) + self.assertIsNone(wr_allocator()) + self.assertIsNotNone(wr_stream()) + + stream = None + gc.collect() + self.assertIsNone(wr_stream()) + self.assertIsNone(wr_camconfig()) + self.assertIsNone(wr_streamconfig()) + + +class SimpleCaptureMethods(CameraTesterBase): + def test_sleep(self): + cm = self.cm + cam = self.cam + + camconfig = cam.generate_configuration([libcam.StreamRole.StillCapture]) + self.assertTrue(camconfig.size == 1) + + streamconfig = camconfig.at(0) + fmts = streamconfig.formats + + ret = cam.configure(camconfig) + self.assertZero(ret) + + stream = streamconfig.stream + + allocator = libcam.FrameBufferAllocator(cam) + ret = allocator.allocate(stream) + self.assertTrue(ret > 0) + + num_bufs = len(allocator.buffers(stream)) + + reqs = [] + for i in range(num_bufs): + req = cam.create_request(i) + self.assertIsNotNone(req) + + buffer = allocator.buffers(stream)[i] + ret = req.add_buffer(stream, buffer) + self.assertZero(ret) + + reqs.append(req) + + buffer = None + + ret = cam.start() + self.assertZero(ret) + + for req in reqs: + ret = cam.queue_request(req) + self.assertZero(ret) + + reqs = None + gc.collect() + + time.sleep(0.5) + + reqs = cm.get_ready_requests() + + self.assertTrue(len(reqs) == num_bufs) + + for i, req in enumerate(reqs): + self.assertTrue(i == req.cookie) + + reqs = None + gc.collect() + + ret = cam.stop() + self.assertZero(ret) + + def test_select(self): + cm = self.cm + cam = self.cam + + camconfig = cam.generate_configuration([libcam.StreamRole.StillCapture]) + self.assertTrue(camconfig.size == 1) + + streamconfig = camconfig.at(0) + fmts = streamconfig.formats + + ret = cam.configure(camconfig) + self.assertZero(ret) + + stream = streamconfig.stream + + allocator = libcam.FrameBufferAllocator(cam) + ret = allocator.allocate(stream) + self.assertTrue(ret > 0) + + num_bufs = len(allocator.buffers(stream)) + + reqs = [] + for i in range(num_bufs): + req = cam.create_request(i) + self.assertIsNotNone(req) + + buffer = allocator.buffers(stream)[i] + ret = req.add_buffer(stream, buffer) + self.assertZero(ret) + + reqs.append(req) + + buffer = None + + ret = cam.start() + self.assertZero(ret) + + for req in reqs: + ret = cam.queue_request(req) + self.assertZero(ret) + + reqs = None + gc.collect() + + sel = selectors.DefaultSelector() + sel.register(cm.efd, selectors.EVENT_READ) + + reqs = [] + + running = True + while running: + events = sel.select() + for key, mask in events: + os.read(key.fileobj, 8) + + ready_reqs = cm.get_ready_requests() + + self.assertTrue(len(ready_reqs) > 0) + + reqs += ready_reqs + + if len(reqs) == num_bufs: + running = False + + self.assertTrue(len(reqs) == num_bufs) + + for i, req in enumerate(reqs): + self.assertTrue(i == req.cookie) + + reqs = None + gc.collect() + + ret = cam.stop() + self.assertZero(ret) + + +# Recursively expand slist's objects into olist, using seen to track already +# processed objects. +def _getr(slist, olist, seen): + for e in slist: + if id(e) in seen: + continue + seen.add(id(e)) + olist.append(e) + tl = gc.get_referents(e) + if tl: + _getr(tl, olist, seen) + + +def get_all_objects(ignored=[]): + gcl = gc.get_objects() + olist = [] + seen = set() + + seen.add(id(gcl)) + seen.add(id(olist)) + seen.add(id(seen)) + seen.update(set([id(o) for o in ignored])) + + _getr(gcl, olist, seen) + + return olist + + +def create_type_count_map(olist): + map = defaultdict(int) + for o in olist: + map[type(o)] += 1 + return map + + +def diff_type_count_maps(before, after): + return [(k, after[k] - before[k]) for k in after if after[k] != before[k]] + + +if __name__ == '__main__': + # \todo This is an attempt to see the Python objects that are not collected, + # but this doesn't work very well, as things always leak a bit. + test_leaks = False + + if test_leaks: + gc.unfreeze() + gc.collect() + + obs_before = get_all_objects() + + unittest.main(exit=False) + + if test_leaks: + gc.unfreeze() + gc.collect() + + obs_after = get_all_objects([obs_before]) + + before = create_type_count_map(obs_before) + after = create_type_count_map(obs_after) + + leaks = diff_type_count_maps(before, after) + if len(leaks) > 0: + print(leaks) From patchwork Fri May 6 14:54:14 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15818 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id CCCA6C326E for ; Fri, 6 May 2022 14:55:19 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 80E0F6564A; Fri, 6 May 2022 16:55:19 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651848919; bh=o+HjeDpitIEhEySH0Qnvhk5AhiQLfKPNXGWrQ1OG9wg=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=Ef3QKZDoOgg1SUAY5xSNOz/+K6rYt2TZHp9GNxPtlrh1qa31v8jx0XR/95p5wVQFA 264cUCRa6b8oGUDGrCORMhyxEyGy0yGxisV9+d+nrbeO7IB9HJR+Rgev5PXrjgjx0C qOlUuybCTjyh5X68KRgGvw4+8oUDtEJn7V0MnOYu0+yEvlEqfBPBwA9i5p+FlsrxW/ 4DIGA3zjCNj0GwLm4xi9CFznwSHOX2fXAN07ufpERGQcEnBu8BGJ3ifm7WBPUFbF0Z +JwuNveZyclG6lZ1D4WHv+HQ1DpK1Tb7kaVLJsa4mwv9KWNdmq/HqyYT3fJoI1yBk7 MGlV8MaCuDiFA== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id C0B4C65652 for ; Fri, 6 May 2022 16:55:13 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="byXAHt3s"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 17B31A43; Fri, 6 May 2022 16:55:13 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651848913; bh=o+HjeDpitIEhEySH0Qnvhk5AhiQLfKPNXGWrQ1OG9wg=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=byXAHt3sVqT9y+4ja0KKmDiFUHpu2e3LW3TrNmGvR/D42AbfcNMLCoBkSJNn8el+N NL2/EIx7hgUNY44QWOAA8PR67l2FHbUyl+qkjyOdCedJ/TV1RDvzWYV9knPhI3E8u0 DMbBEfK1wHEk4In2ReXS3yISAN5E5wdYu/TvPDvM= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Fri, 6 May 2022 17:54:14 +0300 Message-Id: <20220506145414.99039-8-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220506145414.99039-1-tomi.valkeinen@ideasonboard.com> References: <20220506145414.99039-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v8 7/7] py: Add cam.py X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Add cam.py, which mimics the 'cam' tool. Four rendering backends are added: * null - Do nothing * kms - Use KMS with dmabufs * qt - SW render on a Qt window * qtgl - OpenGL render on a Qt window All the renderers handle only a few pixel formats, and especially the GL renderer is just a prototype. Signed-off-by: Tomi Valkeinen --- src/py/cam/cam.py | 484 +++++++++++++++++++++++++++++++++++++++ src/py/cam/cam_kms.py | 183 +++++++++++++++ src/py/cam/cam_null.py | 47 ++++ src/py/cam/cam_qt.py | 354 ++++++++++++++++++++++++++++ src/py/cam/cam_qtgl.py | 385 +++++++++++++++++++++++++++++++ src/py/cam/gl_helpers.py | 74 ++++++ 6 files changed, 1527 insertions(+) create mode 100755 src/py/cam/cam.py create mode 100644 src/py/cam/cam_kms.py create mode 100644 src/py/cam/cam_null.py create mode 100644 src/py/cam/cam_qt.py create mode 100644 src/py/cam/cam_qtgl.py create mode 100644 src/py/cam/gl_helpers.py diff --git a/src/py/cam/cam.py b/src/py/cam/cam.py new file mode 100755 index 00000000..5ea02850 --- /dev/null +++ b/src/py/cam/cam.py @@ -0,0 +1,484 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2021, Tomi Valkeinen + +import argparse +import binascii +import libcamera as libcam +import os +import sys + + +class CustomCameraAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + print(self.dest, values) + + if 'camera' not in namespace or namespace.camera is None: + setattr(namespace, 'camera', []) + + previous = namespace.camera + previous.append((self.dest, values)) + setattr(namespace, 'camera', previous) + + +class CustomAction(argparse.Action): + def __init__(self, option_strings, dest, **kwargs): + super().__init__(option_strings, dest, default={}, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + if len(namespace.camera) == 0: + print(f'Option {option_string} requires a --camera context') + sys.exit(-1) + + if self.type == bool: + values = True + + current = namespace.camera[-1] + + data = getattr(namespace, self.dest) + + if self.nargs == '+': + if current not in data: + data[current] = [] + + data[current] += values + else: + data[current] = values + + +def do_cmd_list(cm): + print('Available cameras:') + + for idx, c in enumerate(cm.cameras): + print(f'{idx + 1}: {c.id}') + + +def do_cmd_list_props(ctx): + camera = ctx['camera'] + + print('Properties for', ctx['id']) + + for name, prop in camera.properties.items(): + print('\t{}: {}'.format(name, prop)) + + +def do_cmd_list_controls(ctx): + camera = ctx['camera'] + + print('Controls for', ctx['id']) + + for name, prop in camera.controls.items(): + print('\t{}: {}'.format(name, prop)) + + +def do_cmd_info(ctx): + camera = ctx['camera'] + + print('Stream info for', ctx['id']) + + roles = [libcam.StreamRole.Viewfinder] + + camconfig = camera.generate_configuration(roles) + if camconfig is None: + raise Exception('Generating config failed') + + for i, stream_config in enumerate(camconfig): + print('\t{}: {}'.format(i, stream_config.to_string())) + + formats = stream_config.formats + for fmt in formats.pixel_formats: + print('\t * Pixelformat:', fmt, formats.range(fmt)) + + for size in formats.sizes(fmt): + print('\t -', size) + + +def acquire(ctx): + camera = ctx['camera'] + + camera.acquire() + + +def release(ctx): + camera = ctx['camera'] + + camera.release() + + +def parse_streams(ctx): + streams = [] + + for stream_desc in ctx['opt-stream']: + stream_opts = {'role': libcam.StreamRole.Viewfinder} + + for stream_opt in stream_desc.split(','): + if stream_opt == 0: + continue + + arr = stream_opt.split('=') + if len(arr) != 2: + print('Bad stream option', stream_opt) + sys.exit(-1) + + key = arr[0] + value = arr[1] + + if key in ['width', 'height']: + value = int(value) + elif key == 'role': + rolemap = { + 'still': libcam.StreamRole.StillCapture, + 'raw': libcam.StreamRole.Raw, + 'video': libcam.StreamRole.VideoRecording, + 'viewfinder': libcam.StreamRole.Viewfinder, + } + + role = rolemap.get(value.lower(), None) + + if role is None: + print('Bad stream role', value) + sys.exit(-1) + + value = role + elif key == 'pixelformat': + pass + else: + print('Bad stream option key', key) + sys.exit(-1) + + stream_opts[key] = value + + streams.append(stream_opts) + + return streams + + +def configure(ctx): + camera = ctx['camera'] + + streams = parse_streams(ctx) + + roles = [opts['role'] for opts in streams] + + camconfig = camera.generate_configuration(roles) + if camconfig is None: + raise Exception('Generating config failed') + + for idx, stream_opts in enumerate(streams): + stream_config = camconfig.at(idx) + + if 'width' in stream_opts and 'height' in stream_opts: + stream_config.size = (stream_opts['width'], stream_opts['height']) + + if 'pixelformat' in stream_opts: + stream_config.pixel_format = stream_opts['pixelformat'] + + stat = camconfig.validate() + + if stat == libcam.CameraConfiguration.Status.Invalid: + print('Camera configuration invalid') + exit(-1) + elif stat == libcam.CameraConfiguration.Status.Adjusted: + if ctx['opt-strict-formats']: + print('Adjusting camera configuration disallowed by --strict-formats argument') + exit(-1) + + print('Camera configuration adjusted') + + r = camera.configure(camconfig) + if r != 0: + raise Exception('Configure failed') + + ctx['stream-names'] = {} + ctx['streams'] = [] + + for idx, stream_config in enumerate(camconfig): + stream = stream_config.stream + ctx['streams'].append(stream) + ctx['stream-names'][stream] = 'stream' + str(idx) + print('{}-{}: stream config {}'.format(ctx['id'], ctx['stream-names'][stream], stream.configuration.to_string())) + + +def alloc_buffers(ctx): + camera = ctx['camera'] + + allocator = libcam.FrameBufferAllocator(camera) + + for idx, stream in enumerate(ctx['streams']): + ret = allocator.allocate(stream) + if ret < 0: + print('Cannot allocate buffers') + exit(-1) + + allocated = len(allocator.buffers(stream)) + + print('{}-{}: Allocated {} buffers'.format(ctx['id'], ctx['stream-names'][stream], allocated)) + + ctx['allocator'] = allocator + + +def create_requests(ctx): + camera = ctx['camera'] + + ctx['requests'] = [] + + # Identify the stream with the least number of buffers + num_bufs = min([len(ctx['allocator'].buffers(stream)) for stream in ctx['streams']]) + + requests = [] + + for buf_num in range(num_bufs): + request = camera.create_request(ctx['idx']) + + if request is None: + print('Can not create request') + exit(-1) + + for stream in ctx['streams']: + buffers = ctx['allocator'].buffers(stream) + buffer = buffers[buf_num] + + ret = request.add_buffer(stream, buffer) + if ret < 0: + print('Can not set buffer for request') + exit(-1) + + requests.append(request) + + ctx['requests'] = requests + + +def start(ctx): + camera = ctx['camera'] + + camera.start() + + +def stop(ctx): + camera = ctx['camera'] + + camera.stop() + + +def queue_requests(ctx): + camera = ctx['camera'] + + for request in ctx['requests']: + camera.queue_request(request) + ctx['reqs-queued'] += 1 + + del ctx['requests'] + + +def capture_init(contexts): + for ctx in contexts: + acquire(ctx) + + for ctx in contexts: + configure(ctx) + + for ctx in contexts: + alloc_buffers(ctx) + + for ctx in contexts: + create_requests(ctx) + + +def capture_start(contexts): + for ctx in contexts: + start(ctx) + + for ctx in contexts: + queue_requests(ctx) + + +# Called from renderer when there is a libcamera event +def event_handler(state): + cm = state['cm'] + contexts = state['contexts'] + + os.read(cm.efd, 8) + + reqs = cm.get_ready_requests() + + for req in reqs: + ctx = next(ctx for ctx in contexts if ctx['idx'] == req.cookie) + request_handler(state, ctx, req) + + running = any(ctx['reqs-completed'] < ctx['opt-capture'] for ctx in contexts) + return running + + +def request_handler(state, ctx, req): + if req.status != libcam.Request.Status.Complete: + raise Exception('{}: Request failed: {}'.format(ctx['id'], req.status)) + + buffers = req.buffers + + # Compute the frame rate. The timestamp is arbitrarily retrieved from + # the first buffer, as all buffers should have matching timestamps. + ts = buffers[next(iter(buffers))].metadata.timestamp + last = ctx.get('last', 0) + fps = 1000000000.0 / (ts - last) if (last != 0 and (ts - last) != 0) else 0 + ctx['last'] = ts + ctx['fps'] = fps + + for stream, fb in buffers.items(): + stream_name = ctx['stream-names'][stream] + + crcs = [] + if ctx['opt-crc']: + with fb.mmap() as mfb: + plane_crcs = [binascii.crc32(p) for p in mfb.planes] + crcs.append(plane_crcs) + + meta = fb.metadata + + print('{:.6f} ({:.2f} fps) {}-{}: seq {}, bytes {}, CRCs {}' + .format(ts / 1000000000, fps, + ctx['id'], stream_name, + meta.sequence, meta.bytesused, + crcs)) + + if ctx['opt-metadata']: + reqmeta = req.metadata + for ctrl, val in reqmeta.items(): + print(f'\t{ctrl} = {val}') + + if ctx['opt-save-frames']: + with fb.mmap() as mfb: + filename = 'frame-{}-{}-{}.data'.format(ctx['id'], stream_name, ctx['reqs-completed']) + with open(filename, 'wb') as f: + for p in mfb.planes: + f.write(p) + + state['renderer'].request_handler(ctx, req) + + ctx['reqs-completed'] += 1 + + +# Called from renderer when it has finished with a request +def request_prcessed(ctx, req): + camera = ctx['camera'] + + if ctx['reqs-queued'] < ctx['opt-capture']: + req.reuse() + camera.queue_request(req) + ctx['reqs-queued'] += 1 + + +def capture_deinit(contexts): + for ctx in contexts: + stop(ctx) + + for ctx in contexts: + release(ctx) + + +def do_cmd_capture(state): + capture_init(state['contexts']) + + renderer = state['renderer'] + + renderer.setup() + + capture_start(state['contexts']) + + renderer.run() + + capture_deinit(state['contexts']) + + +def main(): + parser = argparse.ArgumentParser() + # global options + parser.add_argument('-l', '--list', action='store_true', help='List all cameras') + parser.add_argument('-c', '--camera', type=int, action='extend', nargs=1, default=[], help='Specify which camera to operate on, by index') + parser.add_argument('-p', '--list-properties', action='store_true', help='List cameras properties') + parser.add_argument('--list-controls', action='store_true', help='List cameras controls') + parser.add_argument('-I', '--info', action='store_true', help='Display information about stream(s)') + parser.add_argument('-R', '--renderer', default='null', help='Renderer (null, kms, qt, qtgl)') + + # per camera options + parser.add_argument('-C', '--capture', nargs='?', type=int, const=1000000, action=CustomAction, help='Capture until interrupted by user or until CAPTURE frames captured') + parser.add_argument('--crc', nargs=0, type=bool, action=CustomAction, help='Print CRC32 for captured frames') + parser.add_argument('--save-frames', nargs=0, type=bool, action=CustomAction, help='Save captured frames to files') + parser.add_argument('--metadata', nargs=0, type=bool, action=CustomAction, help='Print the metadata for completed requests') + parser.add_argument('--strict-formats', type=bool, nargs=0, action=CustomAction, help='Do not allow requested stream format(s) to be adjusted') + parser.add_argument('-s', '--stream', nargs='+', action=CustomAction) + args = parser.parse_args() + + cm = libcam.CameraManager.singleton() + + if args.list: + do_cmd_list(cm) + + contexts = [] + + for cam_idx in args.camera: + camera = next((c for i, c in enumerate(cm.cameras) if i + 1 == cam_idx), None) + + if camera is None: + print('Unable to find camera', cam_idx) + return -1 + + contexts.append({ + 'camera': camera, + 'idx': cam_idx, + 'id': 'cam' + str(cam_idx), + 'reqs-queued': 0, + 'reqs-completed': 0, + 'opt-capture': args.capture.get(cam_idx, False), + 'opt-crc': args.crc.get(cam_idx, False), + 'opt-save-frames': args.save_frames.get(cam_idx, False), + 'opt-metadata': args.metadata.get(cam_idx, False), + 'opt-strict-formats': args.strict_formats.get(cam_idx, False), + 'opt-stream': args.stream.get(cam_idx, ['role=viewfinder']), + }) + + for ctx in contexts: + print('Using camera {} as {}'.format(ctx['camera'].id, ctx['id'])) + + for ctx in contexts: + if args.list_properties: + do_cmd_list_props(ctx) + if args.list_controls: + do_cmd_list_controls(ctx) + if args.info: + do_cmd_info(ctx) + + if args.capture: + + state = { + 'cm': cm, + 'contexts': contexts, + 'event_handler': event_handler, + 'request_prcessed': request_prcessed, + } + + if args.renderer == 'null': + import cam_null + renderer = cam_null.NullRenderer(state) + elif args.renderer == 'kms': + import cam_kms + renderer = cam_kms.KMSRenderer(state) + elif args.renderer == 'qt': + import cam_qt + renderer = cam_qt.QtRenderer(state) + elif args.renderer == 'qtgl': + import cam_qtgl + renderer = cam_qtgl.QtRenderer(state) + else: + print('Bad renderer', args.renderer) + return -1 + + state['renderer'] = renderer + + do_cmd_capture(state) + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src/py/cam/cam_kms.py b/src/py/cam/cam_kms.py new file mode 100644 index 00000000..f4ee5a06 --- /dev/null +++ b/src/py/cam/cam_kms.py @@ -0,0 +1,183 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2021, Tomi Valkeinen + +import pykms +import selectors +import sys + +FMT_MAP = { + 'RGB888': pykms.PixelFormat.RGB888, + 'YUYV': pykms.PixelFormat.YUYV, + 'ARGB8888': pykms.PixelFormat.ARGB8888, + 'XRGB8888': pykms.PixelFormat.XRGB8888, +} + + +class KMSRenderer: + def __init__(self, state): + self.state = state + + self.cm = state['cm'] + self.contexts = state['contexts'] + self.running = False + + card = pykms.Card() + + res = pykms.ResourceManager(card) + conn = res.reserve_connector() + crtc = res.reserve_crtc(conn) + mode = conn.get_default_mode() + modeb = mode.to_blob(card) + + req = pykms.AtomicReq(card) + req.add_connector(conn, crtc) + req.add_crtc(crtc, modeb) + r = req.commit_sync(allow_modeset=True) + assert(r == 0) + + self.card = card + self.resman = res + self.crtc = crtc + self.mode = mode + + self.bufqueue = [] + self.current = None + self.next = None + self.cam_2_drm = {} + + # KMS + + def close(self): + req = pykms.AtomicReq(self.card) + for s in self.streams: + req.add_plane(s['plane'], None, None, dst=(0, 0, 0, 0)) + req.commit() + + def add_plane(self, req, stream, fb): + s = next(s for s in self.streams if s['stream'] == stream) + idx = s['idx'] + plane = s['plane'] + + if idx % 2 == 0: + x = 0 + else: + x = self.mode.hdisplay - fb.width + + if idx // 2 == 0: + y = 0 + else: + y = self.mode.vdisplay - fb.height + + req.add_plane(plane, fb, self.crtc, dst=(x, y, fb.width, fb.height)) + + def apply_request(self, drmreq): + + buffers = drmreq['camreq'].buffers + + for stream, fb in buffers.items(): + drmfb = self.cam_2_drm.get(fb, None) + + req = pykms.AtomicReq(self.card) + self.add_plane(req, stream, drmfb) + req.commit() + + def handle_page_flip(self, frame, time): + old = self.current + self.current = self.next + + if len(self.bufqueue) > 0: + self.next = self.bufqueue.pop(0) + else: + self.next = None + + if self.next: + drmreq = self.next + + self.apply_request(drmreq) + + if old: + req = old['camreq'] + ctx = old['camctx'] + self.state['request_prcessed'](ctx, req) + + def queue(self, drmreq): + if not self.next: + self.next = drmreq + self.apply_request(drmreq) + else: + self.bufqueue.append(drmreq) + + # libcamera + + def setup(self): + self.streams = [] + + idx = 0 + for ctx in self.contexts: + for stream in ctx['streams']: + + cfg = stream.configuration + fmt = cfg.pixel_format + fmt = FMT_MAP[fmt] + + plane = self.resman.reserve_generic_plane(self.crtc, fmt) + assert(plane is not None) + + self.streams.append({ + 'idx': idx, + 'stream': stream, + 'plane': plane, + 'fmt': fmt, + 'size': cfg.size, + }) + + for fb in ctx['allocator'].buffers(stream): + w, h = cfg.size + stride = cfg.stride + fd = fb.fd(0) + drmfb = pykms.DmabufFramebuffer(self.card, w, h, fmt, + [fd], [stride], [0]) + self.cam_2_drm[fb] = drmfb + + idx += 1 + + def readdrm(self, fileobj): + for ev in self.card.read_events(): + if ev.type == pykms.DrmEventType.FLIP_COMPLETE: + self.handle_page_flip(ev.seq, ev.time) + + def readcam(self, fd): + self.running = self.state['event_handler'](self.state) + + def readkey(self, fileobj): + sys.stdin.readline() + self.running = False + + def run(self): + print('Capturing...') + + self.running = True + + sel = selectors.DefaultSelector() + sel.register(self.card.fd, selectors.EVENT_READ, self.readdrm) + sel.register(self.cm.efd, selectors.EVENT_READ, self.readcam) + sel.register(sys.stdin, selectors.EVENT_READ, self.readkey) + + print('Press enter to exit') + + while self.running: + events = sel.select() + for key, mask in events: + callback = key.data + callback(key.fileobj) + + print('Exiting...') + + def request_handler(self, ctx, req): + + drmreq = { + 'camctx': ctx, + 'camreq': req, + } + + self.queue(drmreq) diff --git a/src/py/cam/cam_null.py b/src/py/cam/cam_null.py new file mode 100644 index 00000000..391397f6 --- /dev/null +++ b/src/py/cam/cam_null.py @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2021, Tomi Valkeinen + +import selectors +import sys + + +class NullRenderer: + def __init__(self, state): + self.state = state + + self.cm = state['cm'] + self.contexts = state['contexts'] + + self.running = False + + def setup(self): + pass + + def run(self): + print('Capturing...') + + self.running = True + + sel = selectors.DefaultSelector() + sel.register(self.cm.efd, selectors.EVENT_READ, self.readcam) + sel.register(sys.stdin, selectors.EVENT_READ, self.readkey) + + print('Press enter to exit') + + while self.running: + events = sel.select() + for key, mask in events: + callback = key.data + callback(key.fileobj) + + print('Exiting...') + + def readcam(self, fd): + self.running = self.state['event_handler'](self.state) + + def readkey(self, fileobj): + sys.stdin.readline() + self.running = False + + def request_handler(self, ctx, req): + self.state['request_prcessed'](ctx, req) diff --git a/src/py/cam/cam_qt.py b/src/py/cam/cam_qt.py new file mode 100644 index 00000000..40044866 --- /dev/null +++ b/src/py/cam/cam_qt.py @@ -0,0 +1,354 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2021, Tomi Valkeinen +# +# Debayering code from PiCamera documentation + +from io import BytesIO +from numpy.lib.stride_tricks import as_strided +from PIL import Image +from PIL.ImageQt import ImageQt +from PyQt5 import QtCore, QtGui, QtWidgets +import numpy as np +import sys + + +def rgb_to_pix(rgb): + img = Image.frombuffer('RGB', (rgb.shape[1], rgb.shape[0]), rgb) + qim = ImageQt(img).copy() + pix = QtGui.QPixmap.fromImage(qim) + return pix + + +def separate_components(data, r0, g0, g1, b0): + # Now to split the data up into its red, green, and blue components. The + # Bayer pattern of the OV5647 sensor is BGGR. In other words the first + # row contains alternating green/blue elements, the second row contains + # alternating red/green elements, and so on as illustrated below: + # + # GBGBGBGBGBGBGB + # RGRGRGRGRGRGRG + # GBGBGBGBGBGBGB + # RGRGRGRGRGRGRG + # + # Please note that if you use vflip or hflip to change the orientation + # of the capture, you must flip the Bayer pattern accordingly + + rgb = np.zeros(data.shape + (3,), dtype=data.dtype) + rgb[r0[1]::2, r0[0]::2, 0] = data[r0[1]::2, r0[0]::2] # Red + rgb[g0[1]::2, g0[0]::2, 1] = data[g0[1]::2, g0[0]::2] # Green + rgb[g1[1]::2, g1[0]::2, 1] = data[g1[1]::2, g1[0]::2] # Green + rgb[b0[1]::2, b0[0]::2, 2] = data[b0[1]::2, b0[0]::2] # Blue + + return rgb + + +def demosaic(rgb, r0, g0, g1, b0): + # At this point we now have the raw Bayer data with the correct values + # and colors but the data still requires de-mosaicing and + # post-processing. If you wish to do this yourself, end the script here! + # + # Below we present a fairly naive de-mosaic method that simply + # calculates the weighted average of a pixel based on the pixels + # surrounding it. The weighting is provided b0[1] a b0[1]te representation of + # the Bayer filter which we construct first: + + bayer = np.zeros(rgb.shape, dtype=np.uint8) + bayer[r0[1]::2, r0[0]::2, 0] = 1 # Red + bayer[g0[1]::2, g0[0]::2, 1] = 1 # Green + bayer[g1[1]::2, g1[0]::2, 1] = 1 # Green + bayer[b0[1]::2, b0[0]::2, 2] = 1 # Blue + + # Allocate an array to hold our output with the same shape as the input + # data. After this we define the size of window that will be used to + # calculate each weighted average (3x3). Then we pad out the rgb and + # bayer arrays, adding blank pixels at their edges to compensate for the + # size of the window when calculating averages for edge pixels. + + output = np.empty(rgb.shape, dtype=rgb.dtype) + window = (3, 3) + borders = (window[0] - 1, window[1] - 1) + border = (borders[0] // 2, borders[1] // 2) + + # rgb_pad = np.zeros(( + # rgb.shape[0] + borders[0], + # rgb.shape[1] + borders[1], + # rgb.shape[2]), dtype=rgb.dtype) + # rgb_pad[ + # border[0]:rgb_pad.shape[0] - border[0], + # border[1]:rgb_pad.shape[1] - border[1], + # :] = rgb + # rgb = rgb_pad + # + # bayer_pad = np.zeros(( + # bayer.shape[0] + borders[0], + # bayer.shape[1] + borders[1], + # bayer.shape[2]), dtype=bayer.dtype) + # bayer_pad[ + # border[0]:bayer_pad.shape[0] - border[0], + # border[1]:bayer_pad.shape[1] - border[1], + # :] = bayer + # bayer = bayer_pad + + # In numpy >=1.7.0 just use np.pad (version in Raspbian is 1.6.2 at the + # time of writing...) + # + rgb = np.pad(rgb, [ + (border[0], border[0]), + (border[1], border[1]), + (0, 0), + ], 'constant') + bayer = np.pad(bayer, [ + (border[0], border[0]), + (border[1], border[1]), + (0, 0), + ], 'constant') + + # For each plane in the RGB data, we use a nifty numpy trick + # (as_strided) to construct a view over the plane of 3x3 matrices. We do + # the same for the bayer array, then use Einstein summation on each + # (np.sum is simpler, but copies the data so it's slower), and divide + # the results to get our weighted average: + + for plane in range(3): + p = rgb[..., plane] + b = bayer[..., plane] + pview = as_strided(p, shape=( + p.shape[0] - borders[0], + p.shape[1] - borders[1]) + window, strides=p.strides * 2) + bview = as_strided(b, shape=( + b.shape[0] - borders[0], + b.shape[1] - borders[1]) + window, strides=b.strides * 2) + psum = np.einsum('ijkl->ij', pview) + bsum = np.einsum('ijkl->ij', bview) + output[..., plane] = psum // bsum + + return output + + +def to_rgb(fmt, size, data): + w = size[0] + h = size[1] + + if fmt == 'YUYV': + # YUV422 + yuyv = data.reshape((h, w // 2 * 4)) + + # YUV444 + yuv = np.empty((h, w, 3), dtype=np.uint8) + yuv[:, :, 0] = yuyv[:, 0::2] # Y + yuv[:, :, 1] = yuyv[:, 1::4].repeat(2, axis=1) # U + yuv[:, :, 2] = yuyv[:, 3::4].repeat(2, axis=1) # V + + m = np.array([ + [ 1.0, 1.0, 1.0], + [-0.000007154783816076815, -0.3441331386566162, 1.7720025777816772], + [ 1.4019975662231445, -0.7141380310058594 , 0.00001542569043522235] + ]) + + rgb = np.dot(yuv, m) + rgb[:, :, 0] -= 179.45477266423404 + rgb[:, :, 1] += 135.45870971679688 + rgb[:, :, 2] -= 226.8183044444304 + rgb = rgb.astype(np.uint8) + + elif fmt == 'RGB888': + rgb = data.reshape((h, w, 3)) + rgb[:, :, [0, 1, 2]] = rgb[:, :, [2, 1, 0]] + + elif fmt == 'BGR888': + rgb = data.reshape((h, w, 3)) + + elif fmt in ['ARGB8888', 'XRGB8888']: + rgb = data.reshape((h, w, 4)) + rgb = np.flip(rgb, axis=2) + # drop alpha component + rgb = np.delete(rgb, np.s_[0::4], axis=2) + + elif fmt.startswith('S'): + bayer_pattern = fmt[1:5] + bitspp = int(fmt[5:]) + + # TODO: shifting leaves the lowest bits 0 + if bitspp == 8: + data = data.reshape((h, w)) + data = data.astype(np.uint16) << 8 + elif bitspp in [10, 12]: + data = data.view(np.uint16) + data = data.reshape((h, w)) + data = data << (16 - bitspp) + else: + raise Exception('Bad bitspp:' + str(bitspp)) + + idx = bayer_pattern.find('R') + assert(idx != -1) + r0 = (idx % 2, idx // 2) + + idx = bayer_pattern.find('G') + assert(idx != -1) + g0 = (idx % 2, idx // 2) + + idx = bayer_pattern.find('G', idx + 1) + assert(idx != -1) + g1 = (idx % 2, idx // 2) + + idx = bayer_pattern.find('B') + assert(idx != -1) + b0 = (idx % 2, idx // 2) + + rgb = separate_components(data, r0, g0, g1, b0) + rgb = demosaic(rgb, r0, g0, g1, b0) + rgb = (rgb >> 8).astype(np.uint8) + + else: + rgb = None + + return rgb + + +class QtRenderer: + def __init__(self, state): + self.state = state + + self.cm = state['cm'] + self.contexts = state['contexts'] + + def setup(self): + self.app = QtWidgets.QApplication([]) + + windows = [] + + for ctx in self.contexts: + camera = ctx['camera'] + + for stream in ctx['streams']: + fmt = stream.configuration.pixel_format + size = stream.configuration.size + + window = MainWindow(ctx, stream) + window.setAttribute(QtCore.Qt.WA_ShowWithoutActivating) + window.show() + windows.append(window) + + self.windows = windows + + def run(self): + camnotif = QtCore.QSocketNotifier(self.cm.efd, QtCore.QSocketNotifier.Read) + camnotif.activated.connect(lambda x: self.readcam()) + + keynotif = QtCore.QSocketNotifier(sys.stdin.fileno(), QtCore.QSocketNotifier.Read) + keynotif.activated.connect(lambda x: self.readkey()) + + print('Capturing...') + + self.app.exec() + + print('Exiting...') + + def readcam(self): + running = self.state['event_handler'](self.state) + + if not running: + self.app.quit() + + def readkey(self): + sys.stdin.readline() + self.app.quit() + + def request_handler(self, ctx, req): + buffers = req.buffers + + for stream, fb in buffers.items(): + wnd = next(wnd for wnd in self.windows if wnd.stream == stream) + + wnd.handle_request(stream, fb) + + self.state['request_prcessed'](ctx, req) + + def cleanup(self): + for w in self.windows: + w.close() + + +class MainWindow(QtWidgets.QWidget): + def __init__(self, ctx, stream): + super().__init__() + + self.ctx = ctx + self.stream = stream + + self.label = QtWidgets.QLabel() + + windowLayout = QtWidgets.QHBoxLayout() + self.setLayout(windowLayout) + + windowLayout.addWidget(self.label) + + controlsLayout = QtWidgets.QVBoxLayout() + windowLayout.addLayout(controlsLayout) + + windowLayout.addStretch() + + group = QtWidgets.QGroupBox('Info') + groupLayout = QtWidgets.QVBoxLayout() + group.setLayout(groupLayout) + controlsLayout.addWidget(group) + + lab = QtWidgets.QLabel(ctx['id']) + groupLayout.addWidget(lab) + + self.frameLabel = QtWidgets.QLabel() + groupLayout.addWidget(self.frameLabel) + + group = QtWidgets.QGroupBox('Properties') + groupLayout = QtWidgets.QVBoxLayout() + group.setLayout(groupLayout) + controlsLayout.addWidget(group) + + camera = ctx['camera'] + + for k, v in camera.properties.items(): + lab = QtWidgets.QLabel() + lab.setText(k + ' = ' + str(v)) + groupLayout.addWidget(lab) + + group = QtWidgets.QGroupBox('Controls') + groupLayout = QtWidgets.QVBoxLayout() + group.setLayout(groupLayout) + controlsLayout.addWidget(group) + + for k, (min, max, default) in camera.controls.items(): + lab = QtWidgets.QLabel() + lab.setText('{} = {}/{}/{}'.format(k, min, max, default)) + groupLayout.addWidget(lab) + + controlsLayout.addStretch() + + def buf_to_qpixmap(self, stream, fb): + with fb.mmap() as mfb: + cfg = stream.configuration + w, h = cfg.size + pitch = cfg.stride + + if cfg.pixel_format == 'MJPEG': + img = Image.open(BytesIO(mfb.planes[0])) + qim = ImageQt(img).copy() + pix = QtGui.QPixmap.fromImage(qim) + else: + data = np.array(mfb.planes[0], dtype=np.uint8) + rgb = to_rgb(cfg.pixel_format, cfg.size, data) + + if rgb is None: + raise Exception('Format not supported: ' + cfg.pixel_format) + + pix = rgb_to_pix(rgb) + + return pix + + def handle_request(self, stream, fb): + ctx = self.ctx + + pix = self.buf_to_qpixmap(stream, fb) + self.label.setPixmap(pix) + + self.frameLabel.setText('Queued: {}\nDone: {}\nFps: {:.2f}' + .format(ctx['reqs-queued'], ctx['reqs-completed'], ctx['fps'])) diff --git a/src/py/cam/cam_qtgl.py b/src/py/cam/cam_qtgl.py new file mode 100644 index 00000000..37b74d3f --- /dev/null +++ b/src/py/cam/cam_qtgl.py @@ -0,0 +1,385 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2021, Tomi Valkeinen + +from PyQt5 import QtCore, QtWidgets +from PyQt5.QtCore import Qt + +import math +import numpy as np +import os +import sys + +os.environ['PYOPENGL_PLATFORM'] = 'egl' + +import OpenGL +# OpenGL.FULL_LOGGING = True + +from OpenGL import GL as gl +from OpenGL.EGL.EXT.image_dma_buf_import import * +from OpenGL.EGL.KHR.image import * +from OpenGL.EGL.VERSION.EGL_1_0 import * +from OpenGL.EGL.VERSION.EGL_1_2 import * +from OpenGL.EGL.VERSION.EGL_1_3 import * + +from OpenGL.GLES2.OES.EGL_image import * +from OpenGL.GLES2.OES.EGL_image_external import * +from OpenGL.GLES2.VERSION.GLES2_2_0 import * +from OpenGL.GLES3.VERSION.GLES3_3_0 import * + +from OpenGL.GL import shaders + +from gl_helpers import * + +# libcamera format string -> DRM fourcc +FMT_MAP = { + 'RGB888': 'RG24', + 'XRGB8888': 'XR24', + 'ARGB8888': 'AR24', + 'YUYV': 'YUYV', +} + + +class EglState: + def __init__(self): + self.create_display() + self.choose_config() + self.create_context() + self.check_extensions() + + def create_display(self): + xdpy = getEGLNativeDisplay() + dpy = eglGetDisplay(xdpy) + self.display = dpy + + def choose_config(self): + dpy = self.display + + major, minor = EGLint(), EGLint() + + b = eglInitialize(dpy, major, minor) + assert(b) + + print('EGL {} {}'.format( + eglQueryString(dpy, EGL_VENDOR).decode(), + eglQueryString(dpy, EGL_VERSION).decode())) + + check_egl_extensions(dpy, ['EGL_EXT_image_dma_buf_import']) + + b = eglBindAPI(EGL_OPENGL_ES_API) + assert(b) + + def print_config(dpy, cfg): + + def _getconf(dpy, cfg, a): + value = ctypes.c_long() + eglGetConfigAttrib(dpy, cfg, a, value) + return value.value + + getconf = lambda a: _getconf(dpy, cfg, a) + + print('EGL Config {}: color buf {}/{}/{}/{} = {}, depth {}, stencil {}, native visualid {}, native visualtype {}'.format( + getconf(EGL_CONFIG_ID), + getconf(EGL_ALPHA_SIZE), + getconf(EGL_RED_SIZE), + getconf(EGL_GREEN_SIZE), + getconf(EGL_BLUE_SIZE), + getconf(EGL_BUFFER_SIZE), + getconf(EGL_DEPTH_SIZE), + getconf(EGL_STENCIL_SIZE), + getconf(EGL_NATIVE_VISUAL_ID), + getconf(EGL_NATIVE_VISUAL_TYPE))) + + if False: + num_configs = ctypes.c_long() + eglGetConfigs(dpy, None, 0, num_configs) + print('{} configs'.format(num_configs.value)) + + configs = (EGLConfig * num_configs.value)() + eglGetConfigs(dpy, configs, num_configs.value, num_configs) + for config_id in configs: + print_config(dpy, config_id) + + config_attribs = [ + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 0, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_NONE, + ] + + n = EGLint() + configs = (EGLConfig * 1)() + b = eglChooseConfig(dpy, config_attribs, configs, 1, n) + assert(b and n.value == 1) + config = configs[0] + + print('Chosen Config:') + print_config(dpy, config) + + self.config = config + + def create_context(self): + dpy = self.display + + context_attribs = [ + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE, + ] + + context = eglCreateContext(dpy, self.config, EGL_NO_CONTEXT, context_attribs) + assert(context) + + b = eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, context) + assert(b) + + self.context = context + + def check_extensions(self): + check_gl_extensions(['GL_OES_EGL_image']) + + assert(eglCreateImageKHR) + assert(eglDestroyImageKHR) + assert(glEGLImageTargetTexture2DOES) + + +class QtRenderer: + def __init__(self, state): + self.state = state + + def setup(self): + self.app = QtWidgets.QApplication([]) + + window = MainWindow(self.state) + window.setAttribute(QtCore.Qt.WA_ShowWithoutActivating) + window.show() + + self.window = window + + def run(self): + camnotif = QtCore.QSocketNotifier(self.state['cm'].efd, QtCore.QSocketNotifier.Read) + camnotif.activated.connect(lambda x: self.readcam()) + + keynotif = QtCore.QSocketNotifier(sys.stdin.fileno(), QtCore.QSocketNotifier.Read) + keynotif.activated.connect(lambda x: self.readkey()) + + print('Capturing...') + + self.app.exec() + + print('Exiting...') + + def readcam(self): + running = self.state['event_handler'](self.state) + + if not running: + self.app.quit() + + def readkey(self): + sys.stdin.readline() + self.app.quit() + + def request_handler(self, ctx, req): + self.window.handle_request(ctx, req) + + def cleanup(self): + self.window.close() + + +class MainWindow(QtWidgets.QWidget): + def __init__(self, state): + super().__init__() + + self.setAttribute(Qt.WA_PaintOnScreen) + self.setAttribute(Qt.WA_NativeWindow) + + self.state = state + + self.textures = {} + self.reqqueue = {} + self.current = {} + + for ctx in self.state['contexts']: + + self.reqqueue[ctx['idx']] = [] + self.current[ctx['idx']] = [] + + for stream in ctx['streams']: + fmt = stream.configuration.pixel_format + size = stream.configuration.size + + if fmt not in FMT_MAP: + raise Exception('Unsupported pixel format: ' + str(fmt)) + + self.textures[stream] = None + + num_tiles = len(self.textures) + self.num_columns = math.ceil(math.sqrt(num_tiles)) + self.num_rows = math.ceil(num_tiles / self.num_columns) + + self.egl = EglState() + + self.surface = None + + def paintEngine(self): + return None + + def create_surface(self): + native_surface = c_void_p(self.winId().__int__()) + surface = eglCreateWindowSurface(self.egl.display, self.egl.config, + native_surface, None) + + b = eglMakeCurrent(self.egl.display, self.surface, self.surface, self.egl.context) + assert(b) + + self.surface = surface + + def init_gl(self): + self.create_surface() + + vertShaderSrc = ''' + attribute vec2 aPosition; + varying vec2 texcoord; + + void main() + { + gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0); + texcoord.x = aPosition.x; + texcoord.y = 1.0 - aPosition.y; + } + ''' + fragShaderSrc = ''' + #extension GL_OES_EGL_image_external : enable + precision mediump float; + varying vec2 texcoord; + uniform samplerExternalOES texture; + + void main() + { + gl_FragColor = texture2D(texture, texcoord); + } + ''' + + program = shaders.compileProgram( + shaders.compileShader(vertShaderSrc, GL_VERTEX_SHADER), + shaders.compileShader(fragShaderSrc, GL_FRAGMENT_SHADER) + ) + + glUseProgram(program) + + glClearColor(0.5, 0.8, 0.7, 1.0) + + vertPositions = [ + 0.0, 0.0, + 1.0, 0.0, + 1.0, 1.0, + 0.0, 1.0 + ] + + inputAttrib = glGetAttribLocation(program, 'aPosition') + glVertexAttribPointer(inputAttrib, 2, GL_FLOAT, GL_FALSE, 0, vertPositions) + glEnableVertexAttribArray(inputAttrib) + + def create_texture(self, stream, fb): + cfg = stream.configuration + fmt = cfg.pixel_format + fmt = str_to_fourcc(FMT_MAP[fmt]) + w, h = cfg.size + + attribs = [ + EGL_WIDTH, w, + EGL_HEIGHT, h, + EGL_LINUX_DRM_FOURCC_EXT, fmt, + EGL_DMA_BUF_PLANE0_FD_EXT, fb.fd(0), + EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, + EGL_DMA_BUF_PLANE0_PITCH_EXT, cfg.stride, + EGL_NONE, + ] + + image = eglCreateImageKHR(self.egl.display, + EGL_NO_CONTEXT, + EGL_LINUX_DMA_BUF_EXT, + None, + attribs) + assert(image) + + textures = glGenTextures(1) + glBindTexture(GL_TEXTURE_EXTERNAL_OES, textures) + glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) + glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) + glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, image) + + return textures + + def resizeEvent(self, event): + size = event.size() + + print('Resize', size) + + super().resizeEvent(event) + + if self.surface is None: + return + + glViewport(0, 0, size.width() // 2, size.height()) + + def paintEvent(self, event): + if self.surface is None: + self.init_gl() + + for ctx_idx, queue in self.reqqueue.items(): + if len(queue) == 0: + continue + + ctx = next(ctx for ctx in self.state['contexts'] if ctx['idx'] == ctx_idx) + + if self.current[ctx_idx]: + old = self.current[ctx_idx] + self.current[ctx_idx] = None + self.state['request_prcessed'](ctx, old) + + next_req = queue.pop(0) + self.current[ctx_idx] = next_req + + stream, fb = next(iter(next_req.buffers.items())) + + self.textures[stream] = self.create_texture(stream, fb) + + self.paint_gl() + + def paint_gl(self): + b = eglMakeCurrent(self.egl.display, self.surface, self.surface, self.egl.context) + assert(b) + + glClear(GL_COLOR_BUFFER_BIT) + + size = self.size() + + for idx, ctx in enumerate(self.state['contexts']): + for stream in ctx['streams']: + if self.textures[stream] is None: + continue + + w = size.width() // self.num_columns + h = size.height() // self.num_rows + + x = idx % self.num_columns + y = idx // self.num_columns + + x *= w + y *= h + + glViewport(x, y, w, h) + + glBindTexture(GL_TEXTURE_EXTERNAL_OES, self.textures[stream]) + glDrawArrays(GL_TRIANGLE_FAN, 0, 4) + + b = eglSwapBuffers(self.egl.display, self.surface) + assert(b) + + def handle_request(self, ctx, req): + self.reqqueue[ctx['idx']].append(req) + self.update() diff --git a/src/py/cam/gl_helpers.py b/src/py/cam/gl_helpers.py new file mode 100644 index 00000000..b377d735 --- /dev/null +++ b/src/py/cam/gl_helpers.py @@ -0,0 +1,74 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2021, Tomi Valkeinen + +from OpenGL.EGL.VERSION.EGL_1_0 import EGLNativeDisplayType, eglGetProcAddress, eglQueryString, EGL_EXTENSIONS + +from OpenGL.raw.GLES2 import _types as _cs +from OpenGL.GLES2.VERSION.GLES2_2_0 import * +from OpenGL.GLES3.VERSION.GLES3_3_0 import * +from OpenGL import GL as gl + +from ctypes import c_int, c_char_p, c_void_p, cdll, POINTER, util, \ + pointer, CFUNCTYPE, c_bool + + +def getEGLNativeDisplay(): + _x11lib = cdll.LoadLibrary(util.find_library('X11')) + XOpenDisplay = _x11lib.XOpenDisplay + XOpenDisplay.argtypes = [c_char_p] + XOpenDisplay.restype = POINTER(EGLNativeDisplayType) + + xdpy = XOpenDisplay(None) + + +# Hack. PyOpenGL doesn't seem to manage to find glEGLImageTargetTexture2DOES. +def getglEGLImageTargetTexture2DOES(): + funcptr = eglGetProcAddress('glEGLImageTargetTexture2DOES') + prototype = CFUNCTYPE(None, _cs.GLenum, _cs.GLeglImageOES) + return prototype(funcptr) + + +glEGLImageTargetTexture2DOES = getglEGLImageTargetTexture2DOES() + + +def str_to_fourcc(str): + assert(len(str) == 4) + fourcc = 0 + for i, v in enumerate([ord(c) for c in str]): + fourcc |= v << (i * 8) + return fourcc + + +def get_gl_extensions(): + n = GLint() + glGetIntegerv(GL_NUM_EXTENSIONS, n) + gl_extensions = [] + for i in range(n.value): + gl_extensions.append(gl.glGetStringi(GL_EXTENSIONS, i).decode()) + return gl_extensions + + +def check_gl_extensions(required_extensions): + extensions = get_gl_extensions() + + if False: + print('GL EXTENSIONS: ', ' '.join(extensions)) + + for ext in required_extensions: + if ext not in extensions: + raise Exception(ext + ' missing') + + +def get_egl_extensions(egl_display): + return eglQueryString(egl_display, EGL_EXTENSIONS).decode().split(' ') + + +def check_egl_extensions(egl_display, required_extensions): + extensions = get_egl_extensions(egl_display) + + if False: + print('EGL EXTENSIONS: ', ' '.join(extensions)) + + for ext in required_extensions: + if ext not in extensions: + raise Exception(ext + ' missing')