Patch Detail
Show a patch.
GET /api/1.1/patches/15836/?format=api
{ "id": 15836, "url": "https://patchwork.libcamera.org/api/1.1/patches/15836/?format=api", "web_url": "https://patchwork.libcamera.org/patch/15836/", "project": { "id": 1, "url": "https://patchwork.libcamera.org/api/1.1/projects/1/?format=api", "name": "libcamera", "link_name": "libcamera", "list_id": "libcamera_core", "list_email": "libcamera-devel@lists.libcamera.org", "web_url": "", "scm_url": "", "webscm_url": "" }, "msgid": "<20220509101023.35569-8-tomi.valkeinen@ideasonboard.com>", "date": "2022-05-09T10:10:23", "name": "[libcamera-devel,v10,7/7] py: Add cam.py", "commit_ref": null, "pull_url": null, "state": "accepted", "archived": false, "hash": "53eb554871b4ddcb20f78ff435b3beb4472ea643", "submitter": { "id": 109, "url": "https://patchwork.libcamera.org/api/1.1/people/109/?format=api", "name": "Tomi Valkeinen", "email": "tomi.valkeinen@ideasonboard.com" }, "delegate": null, "mbox": "https://patchwork.libcamera.org/patch/15836/mbox/", "series": [ { "id": 3102, "url": "https://patchwork.libcamera.org/api/1.1/series/3102/?format=api", "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=3102", "date": "2022-05-09T10:10:16", "name": "Python bindings", "version": 10, "mbox": "https://patchwork.libcamera.org/series/3102/mbox/" } ], "comments": "https://patchwork.libcamera.org/api/patches/15836/comments/", "check": "pending", "checks": "https://patchwork.libcamera.org/api/patches/15836/checks/", "tags": {}, "headers": { "Return-Path": "<libcamera-devel-bounces@lists.libcamera.org>", "X-Original-To": "parsemail@patchwork.libcamera.org", "Delivered-To": "parsemail@patchwork.libcamera.org", "Received": [ "from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id DBF6BC326C\n\tfor <parsemail@patchwork.libcamera.org>;\n\tMon, 9 May 2022 10:10:57 +0000 (UTC)", "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 8C76B65647;\n\tMon, 9 May 2022 12:10:57 +0200 (CEST)", "from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 1E9F765651\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 9 May 2022 12:10:52 +0200 (CEST)", "from deskari.lan (91-156-85-209.elisa-laajakaista.fi\n\t[91.156.85.209])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 6BCB33E4;\n\tMon, 9 May 2022 12:10:51 +0200 (CEST)" ], "DKIM-Signature": [ "v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1652091057;\n\tbh=T17GewHS1AQPf8sWExWLCcNOBr1udQdUHn0YXBeBJxs=;\n\th=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:\n\tFrom;\n\tb=CKp665Xc2yTpzU8gAB7ryJ7lHgh4oZRXXLChMvZPlrxWvI3JrJFzFRFwHOKihS6rI\n\t00kYET0jBwZCrLZygz5UbNgmTetPewulkdt1UKU33+3DqoMFWqcVGD+d/+BOwRQk8y\n\teWeqndT+inYmiLxQN7bopEKhMb84zID0g8+SYMq8HsrEBBq873u4pzB1NS/lYmjMdY\n\tUdlRhxeLwZ7bHvoTW0Ki5R6cxmwQ4V4wARg77NJeeUPLBnny0bILwJNdFbt/W5zXoZ\n\t05YaOsL3ovTbUZINWe84jeDbegemHBqiOgX/Ha5L10DHU2i8k8Lu2PiXHc8OAQuPsN\n\tbcS1MshkYSK6g==", "v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1652091051;\n\tbh=T17GewHS1AQPf8sWExWLCcNOBr1udQdUHn0YXBeBJxs=;\n\th=From:To:Cc:Subject:Date:In-Reply-To:References:From;\n\tb=GFSRrbiaapzjF/EUvZ7uRGxNBkiwgYc+LaMLQGrjKFzJkhwI/7tyym5HHd7UTG/J7\n\tDkyNXV8KHBdTId1mg+KwKdLmwuKnmhKjE8RqIAh8cp8M7lYrNJdEBY/ng6YCWlOV+m\n\tCWRmatwKTw/LqWg9r2QX9w8c56v8le+VsmmJxXgo=" ], "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (1024-bit key; \n\tunprotected) header.d=ideasonboard.com\n\theader.i=@ideasonboard.com\n\theader.b=\"GFSRrbia\"; dkim-atps=neutral", "To": "libcamera-devel@lists.libcamera.org,\n\tDavid Plowman <david.plowman@raspberrypi.com>,\n\tKieran Bingham <kieran.bingham@ideasonboard.com>,\n\tLaurent Pinchart <laurent.pinchart@ideasonboard.com>,\n\tJacopo Mondi <jacopo@jmondi.org>", "Date": "Mon, 9 May 2022 13:10:23 +0300", "Message-Id": "<20220509101023.35569-8-tomi.valkeinen@ideasonboard.com>", "X-Mailer": "git-send-email 2.34.1", "In-Reply-To": "<20220509101023.35569-1-tomi.valkeinen@ideasonboard.com>", "References": "<20220509101023.35569-1-tomi.valkeinen@ideasonboard.com>", "MIME-Version": "1.0", "Content-Transfer-Encoding": "8bit", "Subject": "[libcamera-devel] [PATCH v10 7/7] py: Add cam.py", "X-BeenThere": "libcamera-devel@lists.libcamera.org", "X-Mailman-Version": "2.1.29", "Precedence": "list", "List-Id": "<libcamera-devel.lists.libcamera.org>", "List-Unsubscribe": "<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>", "List-Archive": "<https://lists.libcamera.org/pipermail/libcamera-devel/>", "List-Post": "<mailto:libcamera-devel@lists.libcamera.org>", "List-Help": "<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>", "List-Subscribe": "<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>", "From": "Tomi Valkeinen via libcamera-devel\n\t<libcamera-devel@lists.libcamera.org>", "Reply-To": "Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>", "Errors-To": "libcamera-devel-bounces@lists.libcamera.org", "Sender": "\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>" }, "content": "Add cam.py, which mimics the 'cam' tool. Four rendering backends are\nadded:\n\n* null - Do nothing\n* kms - Use KMS with dmabufs\n* qt - SW render on a Qt window\n* qtgl - OpenGL render on a Qt window\n\nAll the renderers handle only a few pixel formats, and especially the GL\nrenderer is just a prototype.\n\nSigned-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\nReviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n---\n src/py/cam/cam.py | 475 +++++++++++++++++++++++++++++++++++++++\n src/py/cam/cam_kms.py | 183 +++++++++++++++\n src/py/cam/cam_null.py | 47 ++++\n src/py/cam/cam_qt.py | 354 +++++++++++++++++++++++++++++\n src/py/cam/cam_qtgl.py | 383 +++++++++++++++++++++++++++++++\n src/py/cam/gl_helpers.py | 74 ++++++\n 6 files changed, 1516 insertions(+)\n create mode 100755 src/py/cam/cam.py\n create mode 100644 src/py/cam/cam_kms.py\n create mode 100644 src/py/cam/cam_null.py\n create mode 100644 src/py/cam/cam_qt.py\n create mode 100644 src/py/cam/cam_qtgl.py\n create mode 100644 src/py/cam/gl_helpers.py", "diff": "diff --git a/src/py/cam/cam.py b/src/py/cam/cam.py\nnew file mode 100755\nindex 00000000..012b191c\n--- /dev/null\n+++ b/src/py/cam/cam.py\n@@ -0,0 +1,475 @@\n+#!/usr/bin/env python3\n+\n+# SPDX-License-Identifier: GPL-2.0-or-later\n+# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n+\n+# \\todo Convert ctx and state dicts to proper classes, and move relevant\n+# functions to those classes.\n+\n+import argparse\n+import binascii\n+import libcamera as libcam\n+import os\n+import sys\n+\n+\n+class CustomAction(argparse.Action):\n+ def __init__(self, option_strings, dest, **kwargs):\n+ super().__init__(option_strings, dest, default={}, **kwargs)\n+\n+ def __call__(self, parser, namespace, values, option_string=None):\n+ if len(namespace.camera) == 0:\n+ print(f'Option {option_string} requires a --camera context')\n+ sys.exit(-1)\n+\n+ if self.type == bool:\n+ values = True\n+\n+ current = namespace.camera[-1]\n+\n+ data = getattr(namespace, self.dest)\n+\n+ if self.nargs == '+':\n+ if current not in data:\n+ data[current] = []\n+\n+ data[current] += values\n+ else:\n+ data[current] = values\n+\n+\n+def do_cmd_list(cm):\n+ print('Available cameras:')\n+\n+ for idx, c in enumerate(cm.cameras):\n+ print(f'{idx + 1}: {c.id}')\n+\n+\n+def do_cmd_list_props(ctx):\n+ camera = ctx['camera']\n+\n+ print('Properties for', ctx['id'])\n+\n+ for name, prop in camera.properties.items():\n+ print('\\t{}: {}'.format(name, prop))\n+\n+\n+def do_cmd_list_controls(ctx):\n+ camera = ctx['camera']\n+\n+ print('Controls for', ctx['id'])\n+\n+ for name, prop in camera.controls.items():\n+ print('\\t{}: {}'.format(name, prop))\n+\n+\n+def do_cmd_info(ctx):\n+ camera = ctx['camera']\n+\n+ print('Stream info for', ctx['id'])\n+\n+ roles = [libcam.StreamRole.Viewfinder]\n+\n+ camconfig = camera.generate_configuration(roles)\n+ if camconfig is None:\n+ raise Exception('Generating config failed')\n+\n+ for i, stream_config in enumerate(camconfig):\n+ print('\\t{}: {}'.format(i, stream_config))\n+\n+ formats = stream_config.formats\n+ for fmt in formats.pixel_formats:\n+ print('\\t * Pixelformat:', fmt, formats.range(fmt))\n+\n+ for size in formats.sizes(fmt):\n+ print('\\t -', size)\n+\n+\n+def acquire(ctx):\n+ camera = ctx['camera']\n+\n+ camera.acquire()\n+\n+\n+def release(ctx):\n+ camera = ctx['camera']\n+\n+ camera.release()\n+\n+\n+def parse_streams(ctx):\n+ streams = []\n+\n+ for stream_desc in ctx['opt-stream']:\n+ stream_opts = {'role': libcam.StreamRole.Viewfinder}\n+\n+ for stream_opt in stream_desc.split(','):\n+ if stream_opt == 0:\n+ continue\n+\n+ arr = stream_opt.split('=')\n+ if len(arr) != 2:\n+ print('Bad stream option', stream_opt)\n+ sys.exit(-1)\n+\n+ key = arr[0]\n+ value = arr[1]\n+\n+ if key in ['width', 'height']:\n+ value = int(value)\n+ elif key == 'role':\n+ rolemap = {\n+ 'still': libcam.StreamRole.StillCapture,\n+ 'raw': libcam.StreamRole.Raw,\n+ 'video': libcam.StreamRole.VideoRecording,\n+ 'viewfinder': libcam.StreamRole.Viewfinder,\n+ }\n+\n+ role = rolemap.get(value.lower(), None)\n+\n+ if role is None:\n+ print('Bad stream role', value)\n+ sys.exit(-1)\n+\n+ value = role\n+ elif key == 'pixelformat':\n+ pass\n+ else:\n+ print('Bad stream option key', key)\n+ sys.exit(-1)\n+\n+ stream_opts[key] = value\n+\n+ streams.append(stream_opts)\n+\n+ return streams\n+\n+\n+def configure(ctx):\n+ camera = ctx['camera']\n+\n+ streams = parse_streams(ctx)\n+\n+ roles = [opts['role'] for opts in streams]\n+\n+ camconfig = camera.generate_configuration(roles)\n+ if camconfig is None:\n+ raise Exception('Generating config failed')\n+\n+ for idx, stream_opts in enumerate(streams):\n+ stream_config = camconfig.at(idx)\n+\n+ if 'width' in stream_opts and 'height' in stream_opts:\n+ stream_config.size = (stream_opts['width'], stream_opts['height'])\n+\n+ if 'pixelformat' in stream_opts:\n+ stream_config.pixel_format = stream_opts['pixelformat']\n+\n+ stat = camconfig.validate()\n+\n+ if stat == libcam.CameraConfiguration.Status.Invalid:\n+ print('Camera configuration invalid')\n+ exit(-1)\n+ elif stat == libcam.CameraConfiguration.Status.Adjusted:\n+ if ctx['opt-strict-formats']:\n+ print('Adjusting camera configuration disallowed by --strict-formats argument')\n+ exit(-1)\n+\n+ print('Camera configuration adjusted')\n+\n+ r = camera.configure(camconfig)\n+ if r != 0:\n+ raise Exception('Configure failed')\n+\n+ ctx['stream-names'] = {}\n+ ctx['streams'] = []\n+\n+ for idx, stream_config in enumerate(camconfig):\n+ stream = stream_config.stream\n+ ctx['streams'].append(stream)\n+ ctx['stream-names'][stream] = 'stream' + str(idx)\n+ print('{}-{}: stream config {}'.format(ctx['id'], ctx['stream-names'][stream], stream.configuration))\n+\n+\n+def alloc_buffers(ctx):\n+ camera = ctx['camera']\n+\n+ allocator = libcam.FrameBufferAllocator(camera)\n+\n+ for idx, stream in enumerate(ctx['streams']):\n+ ret = allocator.allocate(stream)\n+ if ret < 0:\n+ print('Cannot allocate buffers')\n+ exit(-1)\n+\n+ allocated = len(allocator.buffers(stream))\n+\n+ print('{}-{}: Allocated {} buffers'.format(ctx['id'], ctx['stream-names'][stream], allocated))\n+\n+ ctx['allocator'] = allocator\n+\n+\n+def create_requests(ctx):\n+ camera = ctx['camera']\n+\n+ ctx['requests'] = []\n+\n+ # Identify the stream with the least number of buffers\n+ num_bufs = min([len(ctx['allocator'].buffers(stream)) for stream in ctx['streams']])\n+\n+ requests = []\n+\n+ for buf_num in range(num_bufs):\n+ request = camera.create_request(ctx['idx'])\n+\n+ if request is None:\n+ print('Can not create request')\n+ exit(-1)\n+\n+ for stream in ctx['streams']:\n+ buffers = ctx['allocator'].buffers(stream)\n+ buffer = buffers[buf_num]\n+\n+ ret = request.add_buffer(stream, buffer)\n+ if ret < 0:\n+ print('Can not set buffer for request')\n+ exit(-1)\n+\n+ requests.append(request)\n+\n+ ctx['requests'] = requests\n+\n+\n+def start(ctx):\n+ camera = ctx['camera']\n+\n+ camera.start()\n+\n+\n+def stop(ctx):\n+ camera = ctx['camera']\n+\n+ camera.stop()\n+\n+\n+def queue_requests(ctx):\n+ camera = ctx['camera']\n+\n+ for request in ctx['requests']:\n+ camera.queue_request(request)\n+ ctx['reqs-queued'] += 1\n+\n+ del ctx['requests']\n+\n+\n+def capture_init(contexts):\n+ for ctx in contexts:\n+ acquire(ctx)\n+\n+ for ctx in contexts:\n+ configure(ctx)\n+\n+ for ctx in contexts:\n+ alloc_buffers(ctx)\n+\n+ for ctx in contexts:\n+ create_requests(ctx)\n+\n+\n+def capture_start(contexts):\n+ for ctx in contexts:\n+ start(ctx)\n+\n+ for ctx in contexts:\n+ queue_requests(ctx)\n+\n+\n+# Called from renderer when there is a libcamera event\n+def event_handler(state):\n+ cm = state['cm']\n+ contexts = state['contexts']\n+\n+ os.read(cm.efd, 8)\n+\n+ reqs = cm.get_ready_requests()\n+\n+ for req in reqs:\n+ ctx = next(ctx for ctx in contexts if ctx['idx'] == req.cookie)\n+ request_handler(state, ctx, req)\n+\n+ running = any(ctx['reqs-completed'] < ctx['opt-capture'] for ctx in contexts)\n+ return running\n+\n+\n+def request_handler(state, ctx, req):\n+ if req.status != libcam.Request.Status.Complete:\n+ raise Exception('{}: Request failed: {}'.format(ctx['id'], req.status))\n+\n+ buffers = req.buffers\n+\n+ # Compute the frame rate. The timestamp is arbitrarily retrieved from\n+ # the first buffer, as all buffers should have matching timestamps.\n+ ts = buffers[next(iter(buffers))].metadata.timestamp\n+ last = ctx.get('last', 0)\n+ fps = 1000000000.0 / (ts - last) if (last != 0 and (ts - last) != 0) else 0\n+ ctx['last'] = ts\n+ ctx['fps'] = fps\n+\n+ for stream, fb in buffers.items():\n+ stream_name = ctx['stream-names'][stream]\n+\n+ crcs = []\n+ if ctx['opt-crc']:\n+ with fb.mmap() as mfb:\n+ plane_crcs = [binascii.crc32(p) for p in mfb.planes]\n+ crcs.append(plane_crcs)\n+\n+ meta = fb.metadata\n+\n+ print('{:.6f} ({:.2f} fps) {}-{}: seq {}, bytes {}, CRCs {}'\n+ .format(ts / 1000000000, fps,\n+ ctx['id'], stream_name,\n+ meta.sequence, meta.bytesused,\n+ crcs))\n+\n+ if ctx['opt-metadata']:\n+ reqmeta = req.metadata\n+ for ctrl, val in reqmeta.items():\n+ print(f'\\t{ctrl} = {val}')\n+\n+ if ctx['opt-save-frames']:\n+ with fb.mmap() as mfb:\n+ filename = 'frame-{}-{}-{}.data'.format(ctx['id'], stream_name, ctx['reqs-completed'])\n+ with open(filename, 'wb') as f:\n+ for p in mfb.planes:\n+ f.write(p)\n+\n+ state['renderer'].request_handler(ctx, req)\n+\n+ ctx['reqs-completed'] += 1\n+\n+\n+# Called from renderer when it has finished with a request\n+def request_prcessed(ctx, req):\n+ camera = ctx['camera']\n+\n+ if ctx['reqs-queued'] < ctx['opt-capture']:\n+ req.reuse()\n+ camera.queue_request(req)\n+ ctx['reqs-queued'] += 1\n+\n+\n+def capture_deinit(contexts):\n+ for ctx in contexts:\n+ stop(ctx)\n+\n+ for ctx in contexts:\n+ release(ctx)\n+\n+\n+def do_cmd_capture(state):\n+ capture_init(state['contexts'])\n+\n+ renderer = state['renderer']\n+\n+ renderer.setup()\n+\n+ capture_start(state['contexts'])\n+\n+ renderer.run()\n+\n+ capture_deinit(state['contexts'])\n+\n+\n+def main():\n+ parser = argparse.ArgumentParser()\n+ # global options\n+ parser.add_argument('-l', '--list', action='store_true', help='List all cameras')\n+ parser.add_argument('-c', '--camera', type=int, action='extend', nargs=1, default=[], help='Specify which camera to operate on, by index')\n+ parser.add_argument('-p', '--list-properties', action='store_true', help='List cameras properties')\n+ parser.add_argument('--list-controls', action='store_true', help='List cameras controls')\n+ parser.add_argument('-I', '--info', action='store_true', help='Display information about stream(s)')\n+ parser.add_argument('-R', '--renderer', default='null', help='Renderer (null, kms, qt, qtgl)')\n+\n+ # per camera options\n+ parser.add_argument('-C', '--capture', nargs='?', type=int, const=1000000, action=CustomAction, help='Capture until interrupted by user or until CAPTURE frames captured')\n+ parser.add_argument('--crc', nargs=0, type=bool, action=CustomAction, help='Print CRC32 for captured frames')\n+ parser.add_argument('--save-frames', nargs=0, type=bool, action=CustomAction, help='Save captured frames to files')\n+ parser.add_argument('--metadata', nargs=0, type=bool, action=CustomAction, help='Print the metadata for completed requests')\n+ parser.add_argument('--strict-formats', type=bool, nargs=0, action=CustomAction, help='Do not allow requested stream format(s) to be adjusted')\n+ parser.add_argument('-s', '--stream', nargs='+', action=CustomAction)\n+ args = parser.parse_args()\n+\n+ cm = libcam.CameraManager.singleton()\n+\n+ if args.list:\n+ do_cmd_list(cm)\n+\n+ contexts = []\n+\n+ for cam_idx in args.camera:\n+ camera = next((c for i, c in enumerate(cm.cameras) if i + 1 == cam_idx), None)\n+\n+ if camera is None:\n+ print('Unable to find camera', cam_idx)\n+ return -1\n+\n+ contexts.append({\n+ 'camera': camera,\n+ 'idx': cam_idx,\n+ 'id': 'cam' + str(cam_idx),\n+ 'reqs-queued': 0,\n+ 'reqs-completed': 0,\n+ 'opt-capture': args.capture.get(cam_idx, False),\n+ 'opt-crc': args.crc.get(cam_idx, False),\n+ 'opt-save-frames': args.save_frames.get(cam_idx, False),\n+ 'opt-metadata': args.metadata.get(cam_idx, False),\n+ 'opt-strict-formats': args.strict_formats.get(cam_idx, False),\n+ 'opt-stream': args.stream.get(cam_idx, ['role=viewfinder']),\n+ })\n+\n+ for ctx in contexts:\n+ print('Using camera {} as {}'.format(ctx['camera'].id, ctx['id']))\n+\n+ for ctx in contexts:\n+ if args.list_properties:\n+ do_cmd_list_props(ctx)\n+ if args.list_controls:\n+ do_cmd_list_controls(ctx)\n+ if args.info:\n+ do_cmd_info(ctx)\n+\n+ if args.capture:\n+\n+ state = {\n+ 'cm': cm,\n+ 'contexts': contexts,\n+ 'event_handler': event_handler,\n+ 'request_prcessed': request_prcessed,\n+ }\n+\n+ if args.renderer == 'null':\n+ import cam_null\n+ renderer = cam_null.NullRenderer(state)\n+ elif args.renderer == 'kms':\n+ import cam_kms\n+ renderer = cam_kms.KMSRenderer(state)\n+ elif args.renderer == 'qt':\n+ import cam_qt\n+ renderer = cam_qt.QtRenderer(state)\n+ elif args.renderer == 'qtgl':\n+ import cam_qtgl\n+ renderer = cam_qtgl.QtRenderer(state)\n+ else:\n+ print('Bad renderer', args.renderer)\n+ return -1\n+\n+ state['renderer'] = renderer\n+\n+ do_cmd_capture(state)\n+\n+ return 0\n+\n+\n+if __name__ == '__main__':\n+ sys.exit(main())\ndiff --git a/src/py/cam/cam_kms.py b/src/py/cam/cam_kms.py\nnew file mode 100644\nindex 00000000..ae6be277\n--- /dev/null\n+++ b/src/py/cam/cam_kms.py\n@@ -0,0 +1,183 @@\n+# SPDX-License-Identifier: GPL-2.0-or-later\n+# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n+\n+import pykms\n+import selectors\n+import sys\n+\n+FMT_MAP = {\n+ 'RGB888': pykms.PixelFormat.RGB888,\n+ 'YUYV': pykms.PixelFormat.YUYV,\n+ 'ARGB8888': pykms.PixelFormat.ARGB8888,\n+ 'XRGB8888': pykms.PixelFormat.XRGB8888,\n+}\n+\n+\n+class KMSRenderer:\n+ def __init__(self, state):\n+ self.state = state\n+\n+ self.cm = state['cm']\n+ self.contexts = state['contexts']\n+ self.running = False\n+\n+ card = pykms.Card()\n+\n+ res = pykms.ResourceManager(card)\n+ conn = res.reserve_connector()\n+ crtc = res.reserve_crtc(conn)\n+ mode = conn.get_default_mode()\n+ modeb = mode.to_blob(card)\n+\n+ req = pykms.AtomicReq(card)\n+ req.add_connector(conn, crtc)\n+ req.add_crtc(crtc, modeb)\n+ r = req.commit_sync(allow_modeset=True)\n+ assert(r == 0)\n+\n+ self.card = card\n+ self.resman = res\n+ self.crtc = crtc\n+ self.mode = mode\n+\n+ self.bufqueue = []\n+ self.current = None\n+ self.next = None\n+ self.cam_2_drm = {}\n+\n+ # KMS\n+\n+ def close(self):\n+ req = pykms.AtomicReq(self.card)\n+ for s in self.streams:\n+ req.add_plane(s['plane'], None, None, dst=(0, 0, 0, 0))\n+ req.commit()\n+\n+ def add_plane(self, req, stream, fb):\n+ s = next(s for s in self.streams if s['stream'] == stream)\n+ idx = s['idx']\n+ plane = s['plane']\n+\n+ if idx % 2 == 0:\n+ x = 0\n+ else:\n+ x = self.mode.hdisplay - fb.width\n+\n+ if idx // 2 == 0:\n+ y = 0\n+ else:\n+ y = self.mode.vdisplay - fb.height\n+\n+ req.add_plane(plane, fb, self.crtc, dst=(x, y, fb.width, fb.height))\n+\n+ def apply_request(self, drmreq):\n+\n+ buffers = drmreq['camreq'].buffers\n+\n+ for stream, fb in buffers.items():\n+ drmfb = self.cam_2_drm.get(fb, None)\n+\n+ req = pykms.AtomicReq(self.card)\n+ self.add_plane(req, stream, drmfb)\n+ req.commit()\n+\n+ def handle_page_flip(self, frame, time):\n+ old = self.current\n+ self.current = self.next\n+\n+ if len(self.bufqueue) > 0:\n+ self.next = self.bufqueue.pop(0)\n+ else:\n+ self.next = None\n+\n+ if self.next:\n+ drmreq = self.next\n+\n+ self.apply_request(drmreq)\n+\n+ if old:\n+ req = old['camreq']\n+ ctx = old['camctx']\n+ self.state['request_prcessed'](ctx, req)\n+\n+ def queue(self, drmreq):\n+ if not self.next:\n+ self.next = drmreq\n+ self.apply_request(drmreq)\n+ else:\n+ self.bufqueue.append(drmreq)\n+\n+ # libcamera\n+\n+ def setup(self):\n+ self.streams = []\n+\n+ idx = 0\n+ for ctx in self.contexts:\n+ for stream in ctx['streams']:\n+\n+ cfg = stream.configuration\n+ fmt = cfg.pixel_format\n+ fmt = FMT_MAP[fmt]\n+\n+ plane = self.resman.reserve_generic_plane(self.crtc, fmt)\n+ assert(plane is not None)\n+\n+ self.streams.append({\n+ 'idx': idx,\n+ 'stream': stream,\n+ 'plane': plane,\n+ 'fmt': fmt,\n+ 'size': cfg.size,\n+ })\n+\n+ for fb in ctx['allocator'].buffers(stream):\n+ w, h = cfg.size\n+ stride = cfg.stride\n+ fd = fb.fd(0)\n+ drmfb = pykms.DmabufFramebuffer(self.card, w, h, fmt,\n+ [fd], [stride], [0])\n+ self.cam_2_drm[fb] = drmfb\n+\n+ idx += 1\n+\n+ def readdrm(self, fileobj):\n+ for ev in self.card.read_events():\n+ if ev.type == pykms.DrmEventType.FLIP_COMPLETE:\n+ self.handle_page_flip(ev.seq, ev.time)\n+\n+ def readcam(self, fd):\n+ self.running = self.state['event_handler'](self.state)\n+\n+ def readkey(self, fileobj):\n+ sys.stdin.readline()\n+ self.running = False\n+\n+ def run(self):\n+ print('Capturing...')\n+\n+ self.running = True\n+\n+ sel = selectors.DefaultSelector()\n+ sel.register(self.card.fd, selectors.EVENT_READ, self.readdrm)\n+ sel.register(self.cm.efd, selectors.EVENT_READ, self.readcam)\n+ sel.register(sys.stdin, selectors.EVENT_READ, self.readkey)\n+\n+ print('Press enter to exit')\n+\n+ while self.running:\n+ events = sel.select()\n+ for key, mask in events:\n+ callback = key.data\n+ callback(key.fileobj)\n+\n+ print('Exiting...')\n+\n+ def request_handler(self, ctx, req):\n+\n+ drmreq = {\n+ 'camctx': ctx,\n+ 'camreq': req,\n+ }\n+\n+ self.queue(drmreq)\ndiff --git a/src/py/cam/cam_null.py b/src/py/cam/cam_null.py\nnew file mode 100644\nindex 00000000..a6da9671\n--- /dev/null\n+++ b/src/py/cam/cam_null.py\n@@ -0,0 +1,47 @@\n+# SPDX-License-Identifier: GPL-2.0-or-later\n+# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n+\n+import selectors\n+import sys\n+\n+\n+class NullRenderer:\n+ def __init__(self, state):\n+ self.state = state\n+\n+ self.cm = state['cm']\n+ self.contexts = state['contexts']\n+\n+ self.running = False\n+\n+ def setup(self):\n+ pass\n+\n+ def run(self):\n+ print('Capturing...')\n+\n+ self.running = True\n+\n+ sel = selectors.DefaultSelector()\n+ sel.register(self.cm.efd, selectors.EVENT_READ, self.readcam)\n+ sel.register(sys.stdin, selectors.EVENT_READ, self.readkey)\n+\n+ print('Press enter to exit')\n+\n+ while self.running:\n+ events = sel.select()\n+ for key, mask in events:\n+ callback = key.data\n+ callback(key.fileobj)\n+\n+ print('Exiting...')\n+\n+ def readcam(self, fd):\n+ self.running = self.state['event_handler'](self.state)\n+\n+ def readkey(self, fileobj):\n+ sys.stdin.readline()\n+ self.running = False\n+\n+ def request_handler(self, ctx, req):\n+ self.state['request_prcessed'](ctx, req)\ndiff --git a/src/py/cam/cam_qt.py b/src/py/cam/cam_qt.py\nnew file mode 100644\nindex 00000000..5753f0b2\n--- /dev/null\n+++ b/src/py/cam/cam_qt.py\n@@ -0,0 +1,354 @@\n+# SPDX-License-Identifier: GPL-2.0-or-later\n+# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n+#\n+# Debayering code from PiCamera documentation\n+\n+from io import BytesIO\n+from numpy.lib.stride_tricks import as_strided\n+from PIL import Image\n+from PIL.ImageQt import ImageQt\n+from PyQt5 import QtCore, QtGui, QtWidgets\n+import numpy as np\n+import sys\n+\n+\n+def rgb_to_pix(rgb):\n+ img = Image.frombuffer('RGB', (rgb.shape[1], rgb.shape[0]), rgb)\n+ qim = ImageQt(img).copy()\n+ pix = QtGui.QPixmap.fromImage(qim)\n+ return pix\n+\n+\n+def separate_components(data, r0, g0, g1, b0):\n+ # Now to split the data up into its red, green, and blue components. The\n+ # Bayer pattern of the OV5647 sensor is BGGR. In other words the first\n+ # row contains alternating green/blue elements, the second row contains\n+ # alternating red/green elements, and so on as illustrated below:\n+ #\n+ # GBGBGBGBGBGBGB\n+ # RGRGRGRGRGRGRG\n+ # GBGBGBGBGBGBGB\n+ # RGRGRGRGRGRGRG\n+ #\n+ # Please note that if you use vflip or hflip to change the orientation\n+ # of the capture, you must flip the Bayer pattern accordingly\n+\n+ rgb = np.zeros(data.shape + (3,), dtype=data.dtype)\n+ rgb[r0[1]::2, r0[0]::2, 0] = data[r0[1]::2, r0[0]::2] # Red\n+ rgb[g0[1]::2, g0[0]::2, 1] = data[g0[1]::2, g0[0]::2] # Green\n+ rgb[g1[1]::2, g1[0]::2, 1] = data[g1[1]::2, g1[0]::2] # Green\n+ rgb[b0[1]::2, b0[0]::2, 2] = data[b0[1]::2, b0[0]::2] # Blue\n+\n+ return rgb\n+\n+\n+def demosaic(rgb, r0, g0, g1, b0):\n+ # At this point we now have the raw Bayer data with the correct values\n+ # and colors but the data still requires de-mosaicing and\n+ # post-processing. If you wish to do this yourself, end the script here!\n+ #\n+ # Below we present a fairly naive de-mosaic method that simply\n+ # calculates the weighted average of a pixel based on the pixels\n+ # surrounding it. The weighting is provided b0[1] a b0[1]te representation of\n+ # the Bayer filter which we construct first:\n+\n+ bayer = np.zeros(rgb.shape, dtype=np.uint8)\n+ bayer[r0[1]::2, r0[0]::2, 0] = 1 # Red\n+ bayer[g0[1]::2, g0[0]::2, 1] = 1 # Green\n+ bayer[g1[1]::2, g1[0]::2, 1] = 1 # Green\n+ bayer[b0[1]::2, b0[0]::2, 2] = 1 # Blue\n+\n+ # Allocate an array to hold our output with the same shape as the input\n+ # data. After this we define the size of window that will be used to\n+ # calculate each weighted average (3x3). Then we pad out the rgb and\n+ # bayer arrays, adding blank pixels at their edges to compensate for the\n+ # size of the window when calculating averages for edge pixels.\n+\n+ output = np.empty(rgb.shape, dtype=rgb.dtype)\n+ window = (3, 3)\n+ borders = (window[0] - 1, window[1] - 1)\n+ border = (borders[0] // 2, borders[1] // 2)\n+\n+ # rgb_pad = np.zeros((\n+ # rgb.shape[0] + borders[0],\n+ # rgb.shape[1] + borders[1],\n+ # rgb.shape[2]), dtype=rgb.dtype)\n+ # rgb_pad[\n+ # border[0]:rgb_pad.shape[0] - border[0],\n+ # border[1]:rgb_pad.shape[1] - border[1],\n+ # :] = rgb\n+ # rgb = rgb_pad\n+ #\n+ # bayer_pad = np.zeros((\n+ # bayer.shape[0] + borders[0],\n+ # bayer.shape[1] + borders[1],\n+ # bayer.shape[2]), dtype=bayer.dtype)\n+ # bayer_pad[\n+ # border[0]:bayer_pad.shape[0] - border[0],\n+ # border[1]:bayer_pad.shape[1] - border[1],\n+ # :] = bayer\n+ # bayer = bayer_pad\n+\n+ # In numpy >=1.7.0 just use np.pad (version in Raspbian is 1.6.2 at the\n+ # time of writing...)\n+ #\n+ rgb = np.pad(rgb, [\n+ (border[0], border[0]),\n+ (border[1], border[1]),\n+ (0, 0),\n+ ], 'constant')\n+ bayer = np.pad(bayer, [\n+ (border[0], border[0]),\n+ (border[1], border[1]),\n+ (0, 0),\n+ ], 'constant')\n+\n+ # For each plane in the RGB data, we use a nifty numpy trick\n+ # (as_strided) to construct a view over the plane of 3x3 matrices. We do\n+ # the same for the bayer array, then use Einstein summation on each\n+ # (np.sum is simpler, but copies the data so it's slower), and divide\n+ # the results to get our weighted average:\n+\n+ for plane in range(3):\n+ p = rgb[..., plane]\n+ b = bayer[..., plane]\n+ pview = as_strided(p, shape=(\n+ p.shape[0] - borders[0],\n+ p.shape[1] - borders[1]) + window, strides=p.strides * 2)\n+ bview = as_strided(b, shape=(\n+ b.shape[0] - borders[0],\n+ b.shape[1] - borders[1]) + window, strides=b.strides * 2)\n+ psum = np.einsum('ijkl->ij', pview)\n+ bsum = np.einsum('ijkl->ij', bview)\n+ output[..., plane] = psum // bsum\n+\n+ return output\n+\n+\n+def to_rgb(fmt, size, data):\n+ w = size[0]\n+ h = size[1]\n+\n+ if fmt == 'YUYV':\n+ # YUV422\n+ yuyv = data.reshape((h, w // 2 * 4))\n+\n+ # YUV444\n+ yuv = np.empty((h, w, 3), dtype=np.uint8)\n+ yuv[:, :, 0] = yuyv[:, 0::2] # Y\n+ yuv[:, :, 1] = yuyv[:, 1::4].repeat(2, axis=1) # U\n+ yuv[:, :, 2] = yuyv[:, 3::4].repeat(2, axis=1) # V\n+\n+ m = np.array([\n+ [1.0, 1.0, 1.0],\n+ [-0.000007154783816076815, -0.3441331386566162, 1.7720025777816772],\n+ [1.4019975662231445, -0.7141380310058594, 0.00001542569043522235]\n+ ])\n+\n+ rgb = np.dot(yuv, m)\n+ rgb[:, :, 0] -= 179.45477266423404\n+ rgb[:, :, 1] += 135.45870971679688\n+ rgb[:, :, 2] -= 226.8183044444304\n+ rgb = rgb.astype(np.uint8)\n+\n+ elif fmt == 'RGB888':\n+ rgb = data.reshape((h, w, 3))\n+ rgb[:, :, [0, 1, 2]] = rgb[:, :, [2, 1, 0]]\n+\n+ elif fmt == 'BGR888':\n+ rgb = data.reshape((h, w, 3))\n+\n+ elif fmt in ['ARGB8888', 'XRGB8888']:\n+ rgb = data.reshape((h, w, 4))\n+ rgb = np.flip(rgb, axis=2)\n+ # drop alpha component\n+ rgb = np.delete(rgb, np.s_[0::4], axis=2)\n+\n+ elif fmt.startswith('S'):\n+ bayer_pattern = fmt[1:5]\n+ bitspp = int(fmt[5:])\n+\n+ # TODO: shifting leaves the lowest bits 0\n+ if bitspp == 8:\n+ data = data.reshape((h, w))\n+ data = data.astype(np.uint16) << 8\n+ elif bitspp in [10, 12]:\n+ data = data.view(np.uint16)\n+ data = data.reshape((h, w))\n+ data = data << (16 - bitspp)\n+ else:\n+ raise Exception('Bad bitspp:' + str(bitspp))\n+\n+ idx = bayer_pattern.find('R')\n+ assert(idx != -1)\n+ r0 = (idx % 2, idx // 2)\n+\n+ idx = bayer_pattern.find('G')\n+ assert(idx != -1)\n+ g0 = (idx % 2, idx // 2)\n+\n+ idx = bayer_pattern.find('G', idx + 1)\n+ assert(idx != -1)\n+ g1 = (idx % 2, idx // 2)\n+\n+ idx = bayer_pattern.find('B')\n+ assert(idx != -1)\n+ b0 = (idx % 2, idx // 2)\n+\n+ rgb = separate_components(data, r0, g0, g1, b0)\n+ rgb = demosaic(rgb, r0, g0, g1, b0)\n+ rgb = (rgb >> 8).astype(np.uint8)\n+\n+ else:\n+ rgb = None\n+\n+ return rgb\n+\n+\n+class QtRenderer:\n+ def __init__(self, state):\n+ self.state = state\n+\n+ self.cm = state['cm']\n+ self.contexts = state['contexts']\n+\n+ def setup(self):\n+ self.app = QtWidgets.QApplication([])\n+\n+ windows = []\n+\n+ for ctx in self.contexts:\n+ camera = ctx['camera']\n+\n+ for stream in ctx['streams']:\n+ fmt = stream.configuration.pixel_format\n+ size = stream.configuration.size\n+\n+ window = MainWindow(ctx, stream)\n+ window.setAttribute(QtCore.Qt.WA_ShowWithoutActivating)\n+ window.show()\n+ windows.append(window)\n+\n+ self.windows = windows\n+\n+ def run(self):\n+ camnotif = QtCore.QSocketNotifier(self.cm.efd, QtCore.QSocketNotifier.Read)\n+ camnotif.activated.connect(lambda x: self.readcam())\n+\n+ keynotif = QtCore.QSocketNotifier(sys.stdin.fileno(), QtCore.QSocketNotifier.Read)\n+ keynotif.activated.connect(lambda x: self.readkey())\n+\n+ print('Capturing...')\n+\n+ self.app.exec()\n+\n+ print('Exiting...')\n+\n+ def readcam(self):\n+ running = self.state['event_handler'](self.state)\n+\n+ if not running:\n+ self.app.quit()\n+\n+ def readkey(self):\n+ sys.stdin.readline()\n+ self.app.quit()\n+\n+ def request_handler(self, ctx, req):\n+ buffers = req.buffers\n+\n+ for stream, fb in buffers.items():\n+ wnd = next(wnd for wnd in self.windows if wnd.stream == stream)\n+\n+ wnd.handle_request(stream, fb)\n+\n+ self.state['request_prcessed'](ctx, req)\n+\n+ def cleanup(self):\n+ for w in self.windows:\n+ w.close()\n+\n+\n+class MainWindow(QtWidgets.QWidget):\n+ def __init__(self, ctx, stream):\n+ super().__init__()\n+\n+ self.ctx = ctx\n+ self.stream = stream\n+\n+ self.label = QtWidgets.QLabel()\n+\n+ windowLayout = QtWidgets.QHBoxLayout()\n+ self.setLayout(windowLayout)\n+\n+ windowLayout.addWidget(self.label)\n+\n+ controlsLayout = QtWidgets.QVBoxLayout()\n+ windowLayout.addLayout(controlsLayout)\n+\n+ windowLayout.addStretch()\n+\n+ group = QtWidgets.QGroupBox('Info')\n+ groupLayout = QtWidgets.QVBoxLayout()\n+ group.setLayout(groupLayout)\n+ controlsLayout.addWidget(group)\n+\n+ lab = QtWidgets.QLabel(ctx['id'])\n+ groupLayout.addWidget(lab)\n+\n+ self.frameLabel = QtWidgets.QLabel()\n+ groupLayout.addWidget(self.frameLabel)\n+\n+ group = QtWidgets.QGroupBox('Properties')\n+ groupLayout = QtWidgets.QVBoxLayout()\n+ group.setLayout(groupLayout)\n+ controlsLayout.addWidget(group)\n+\n+ camera = ctx['camera']\n+\n+ for k, v in camera.properties.items():\n+ lab = QtWidgets.QLabel()\n+ lab.setText(k + ' = ' + str(v))\n+ groupLayout.addWidget(lab)\n+\n+ group = QtWidgets.QGroupBox('Controls')\n+ groupLayout = QtWidgets.QVBoxLayout()\n+ group.setLayout(groupLayout)\n+ controlsLayout.addWidget(group)\n+\n+ for k, (min, max, default) in camera.controls.items():\n+ lab = QtWidgets.QLabel()\n+ lab.setText('{} = {}/{}/{}'.format(k, min, max, default))\n+ groupLayout.addWidget(lab)\n+\n+ controlsLayout.addStretch()\n+\n+ def buf_to_qpixmap(self, stream, fb):\n+ with fb.mmap() as mfb:\n+ cfg = stream.configuration\n+ w, h = cfg.size\n+ pitch = cfg.stride\n+\n+ if cfg.pixel_format == 'MJPEG':\n+ img = Image.open(BytesIO(mfb.planes[0]))\n+ qim = ImageQt(img).copy()\n+ pix = QtGui.QPixmap.fromImage(qim)\n+ else:\n+ data = np.array(mfb.planes[0], dtype=np.uint8)\n+ rgb = to_rgb(cfg.pixel_format, cfg.size, data)\n+\n+ if rgb is None:\n+ raise Exception('Format not supported: ' + cfg.pixel_format)\n+\n+ pix = rgb_to_pix(rgb)\n+\n+ return pix\n+\n+ def handle_request(self, stream, fb):\n+ ctx = self.ctx\n+\n+ pix = self.buf_to_qpixmap(stream, fb)\n+ self.label.setPixmap(pix)\n+\n+ self.frameLabel.setText('Queued: {}\\nDone: {}\\nFps: {:.2f}'\n+ .format(ctx['reqs-queued'], ctx['reqs-completed'], ctx['fps']))\ndiff --git a/src/py/cam/cam_qtgl.py b/src/py/cam/cam_qtgl.py\nnew file mode 100644\nindex 00000000..8a95994e\n--- /dev/null\n+++ b/src/py/cam/cam_qtgl.py\n@@ -0,0 +1,383 @@\n+# SPDX-License-Identifier: GPL-2.0-or-later\n+# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n+\n+from PyQt5 import QtCore, QtWidgets\n+from PyQt5.QtCore import Qt\n+\n+import math\n+import numpy as np\n+import os\n+import sys\n+\n+os.environ['PYOPENGL_PLATFORM'] = 'egl'\n+\n+import OpenGL\n+# OpenGL.FULL_LOGGING = True\n+\n+from OpenGL import GL as gl\n+from OpenGL.EGL.EXT.image_dma_buf_import import *\n+from OpenGL.EGL.KHR.image import *\n+from OpenGL.EGL.VERSION.EGL_1_0 import *\n+from OpenGL.EGL.VERSION.EGL_1_2 import *\n+from OpenGL.EGL.VERSION.EGL_1_3 import *\n+\n+from OpenGL.GLES2.OES.EGL_image import *\n+from OpenGL.GLES2.OES.EGL_image_external import *\n+from OpenGL.GLES2.VERSION.GLES2_2_0 import *\n+from OpenGL.GLES3.VERSION.GLES3_3_0 import *\n+\n+from OpenGL.GL import shaders\n+\n+from gl_helpers import *\n+\n+# libcamera format string -> DRM fourcc\n+FMT_MAP = {\n+ 'RGB888': 'RG24',\n+ 'XRGB8888': 'XR24',\n+ 'ARGB8888': 'AR24',\n+ 'YUYV': 'YUYV',\n+}\n+\n+\n+class EglState:\n+ def __init__(self):\n+ self.create_display()\n+ self.choose_config()\n+ self.create_context()\n+ self.check_extensions()\n+\n+ def create_display(self):\n+ xdpy = getEGLNativeDisplay()\n+ dpy = eglGetDisplay(xdpy)\n+ self.display = dpy\n+\n+ def choose_config(self):\n+ dpy = self.display\n+\n+ major, minor = EGLint(), EGLint()\n+\n+ b = eglInitialize(dpy, major, minor)\n+ assert(b)\n+\n+ print('EGL {} {}'.format(\n+ eglQueryString(dpy, EGL_VENDOR).decode(),\n+ eglQueryString(dpy, EGL_VERSION).decode()))\n+\n+ check_egl_extensions(dpy, ['EGL_EXT_image_dma_buf_import'])\n+\n+ b = eglBindAPI(EGL_OPENGL_ES_API)\n+ assert(b)\n+\n+ def print_config(dpy, cfg):\n+\n+ def getconf(a):\n+ value = ctypes.c_long()\n+ eglGetConfigAttrib(dpy, cfg, a, value)\n+ return value.value\n+\n+ print('EGL Config {}: color buf {}/{}/{}/{} = {}, depth {}, stencil {}, native visualid {}, native visualtype {}'.format(\n+ getconf(EGL_CONFIG_ID),\n+ getconf(EGL_ALPHA_SIZE),\n+ getconf(EGL_RED_SIZE),\n+ getconf(EGL_GREEN_SIZE),\n+ getconf(EGL_BLUE_SIZE),\n+ getconf(EGL_BUFFER_SIZE),\n+ getconf(EGL_DEPTH_SIZE),\n+ getconf(EGL_STENCIL_SIZE),\n+ getconf(EGL_NATIVE_VISUAL_ID),\n+ getconf(EGL_NATIVE_VISUAL_TYPE)))\n+\n+ if False:\n+ num_configs = ctypes.c_long()\n+ eglGetConfigs(dpy, None, 0, num_configs)\n+ print('{} configs'.format(num_configs.value))\n+\n+ configs = (EGLConfig * num_configs.value)()\n+ eglGetConfigs(dpy, configs, num_configs.value, num_configs)\n+ for config_id in configs:\n+ print_config(dpy, config_id)\n+\n+ config_attribs = [\n+ EGL_SURFACE_TYPE, EGL_WINDOW_BIT,\n+ EGL_RED_SIZE, 8,\n+ EGL_GREEN_SIZE, 8,\n+ EGL_BLUE_SIZE, 8,\n+ EGL_ALPHA_SIZE, 0,\n+ EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,\n+ EGL_NONE,\n+ ]\n+\n+ n = EGLint()\n+ configs = (EGLConfig * 1)()\n+ b = eglChooseConfig(dpy, config_attribs, configs, 1, n)\n+ assert(b and n.value == 1)\n+ config = configs[0]\n+\n+ print('Chosen Config:')\n+ print_config(dpy, config)\n+\n+ self.config = config\n+\n+ def create_context(self):\n+ dpy = self.display\n+\n+ context_attribs = [\n+ EGL_CONTEXT_CLIENT_VERSION, 2,\n+ EGL_NONE,\n+ ]\n+\n+ context = eglCreateContext(dpy, self.config, EGL_NO_CONTEXT, context_attribs)\n+ assert(context)\n+\n+ b = eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, context)\n+ assert(b)\n+\n+ self.context = context\n+\n+ def check_extensions(self):\n+ check_gl_extensions(['GL_OES_EGL_image'])\n+\n+ assert(eglCreateImageKHR)\n+ assert(eglDestroyImageKHR)\n+ assert(glEGLImageTargetTexture2DOES)\n+\n+\n+class QtRenderer:\n+ def __init__(self, state):\n+ self.state = state\n+\n+ def setup(self):\n+ self.app = QtWidgets.QApplication([])\n+\n+ window = MainWindow(self.state)\n+ window.setAttribute(QtCore.Qt.WA_ShowWithoutActivating)\n+ window.show()\n+\n+ self.window = window\n+\n+ def run(self):\n+ camnotif = QtCore.QSocketNotifier(self.state['cm'].efd, QtCore.QSocketNotifier.Read)\n+ camnotif.activated.connect(lambda x: self.readcam())\n+\n+ keynotif = QtCore.QSocketNotifier(sys.stdin.fileno(), QtCore.QSocketNotifier.Read)\n+ keynotif.activated.connect(lambda x: self.readkey())\n+\n+ print('Capturing...')\n+\n+ self.app.exec()\n+\n+ print('Exiting...')\n+\n+ def readcam(self):\n+ running = self.state['event_handler'](self.state)\n+\n+ if not running:\n+ self.app.quit()\n+\n+ def readkey(self):\n+ sys.stdin.readline()\n+ self.app.quit()\n+\n+ def request_handler(self, ctx, req):\n+ self.window.handle_request(ctx, req)\n+\n+ def cleanup(self):\n+ self.window.close()\n+\n+\n+class MainWindow(QtWidgets.QWidget):\n+ def __init__(self, state):\n+ super().__init__()\n+\n+ self.setAttribute(Qt.WA_PaintOnScreen)\n+ self.setAttribute(Qt.WA_NativeWindow)\n+\n+ self.state = state\n+\n+ self.textures = {}\n+ self.reqqueue = {}\n+ self.current = {}\n+\n+ for ctx in self.state['contexts']:\n+\n+ self.reqqueue[ctx['idx']] = []\n+ self.current[ctx['idx']] = []\n+\n+ for stream in ctx['streams']:\n+ fmt = stream.configuration.pixel_format\n+ size = stream.configuration.size\n+\n+ if fmt not in FMT_MAP:\n+ raise Exception('Unsupported pixel format: ' + str(fmt))\n+\n+ self.textures[stream] = None\n+\n+ num_tiles = len(self.textures)\n+ self.num_columns = math.ceil(math.sqrt(num_tiles))\n+ self.num_rows = math.ceil(num_tiles / self.num_columns)\n+\n+ self.egl = EglState()\n+\n+ self.surface = None\n+\n+ def paintEngine(self):\n+ return None\n+\n+ def create_surface(self):\n+ native_surface = c_void_p(self.winId().__int__())\n+ surface = eglCreateWindowSurface(self.egl.display, self.egl.config,\n+ native_surface, None)\n+\n+ b = eglMakeCurrent(self.egl.display, self.surface, self.surface, self.egl.context)\n+ assert(b)\n+\n+ self.surface = surface\n+\n+ def init_gl(self):\n+ self.create_surface()\n+\n+ vertShaderSrc = '''\n+ attribute vec2 aPosition;\n+ varying vec2 texcoord;\n+\n+ void main()\n+ {\n+ gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);\n+ texcoord.x = aPosition.x;\n+ texcoord.y = 1.0 - aPosition.y;\n+ }\n+ '''\n+ fragShaderSrc = '''\n+ #extension GL_OES_EGL_image_external : enable\n+ precision mediump float;\n+ varying vec2 texcoord;\n+ uniform samplerExternalOES texture;\n+\n+ void main()\n+ {\n+ gl_FragColor = texture2D(texture, texcoord);\n+ }\n+ '''\n+\n+ program = shaders.compileProgram(\n+ shaders.compileShader(vertShaderSrc, GL_VERTEX_SHADER),\n+ shaders.compileShader(fragShaderSrc, GL_FRAGMENT_SHADER)\n+ )\n+\n+ glUseProgram(program)\n+\n+ glClearColor(0.5, 0.8, 0.7, 1.0)\n+\n+ vertPositions = [\n+ 0.0, 0.0,\n+ 1.0, 0.0,\n+ 1.0, 1.0,\n+ 0.0, 1.0\n+ ]\n+\n+ inputAttrib = glGetAttribLocation(program, 'aPosition')\n+ glVertexAttribPointer(inputAttrib, 2, GL_FLOAT, GL_FALSE, 0, vertPositions)\n+ glEnableVertexAttribArray(inputAttrib)\n+\n+ def create_texture(self, stream, fb):\n+ cfg = stream.configuration\n+ fmt = cfg.pixel_format\n+ fmt = str_to_fourcc(FMT_MAP[fmt])\n+ w, h = cfg.size\n+\n+ attribs = [\n+ EGL_WIDTH, w,\n+ EGL_HEIGHT, h,\n+ EGL_LINUX_DRM_FOURCC_EXT, fmt,\n+ EGL_DMA_BUF_PLANE0_FD_EXT, fb.fd(0),\n+ EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,\n+ EGL_DMA_BUF_PLANE0_PITCH_EXT, cfg.stride,\n+ EGL_NONE,\n+ ]\n+\n+ image = eglCreateImageKHR(self.egl.display,\n+ EGL_NO_CONTEXT,\n+ EGL_LINUX_DMA_BUF_EXT,\n+ None,\n+ attribs)\n+ assert(image)\n+\n+ textures = glGenTextures(1)\n+ glBindTexture(GL_TEXTURE_EXTERNAL_OES, textures)\n+ glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR)\n+ glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR)\n+ glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)\n+ glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)\n+ glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, image)\n+\n+ return textures\n+\n+ def resizeEvent(self, event):\n+ size = event.size()\n+\n+ print('Resize', size)\n+\n+ super().resizeEvent(event)\n+\n+ if self.surface is None:\n+ return\n+\n+ glViewport(0, 0, size.width() // 2, size.height())\n+\n+ def paintEvent(self, event):\n+ if self.surface is None:\n+ self.init_gl()\n+\n+ for ctx_idx, queue in self.reqqueue.items():\n+ if len(queue) == 0:\n+ continue\n+\n+ ctx = next(ctx for ctx in self.state['contexts'] if ctx['idx'] == ctx_idx)\n+\n+ if self.current[ctx_idx]:\n+ old = self.current[ctx_idx]\n+ self.current[ctx_idx] = None\n+ self.state['request_prcessed'](ctx, old)\n+\n+ next_req = queue.pop(0)\n+ self.current[ctx_idx] = next_req\n+\n+ stream, fb = next(iter(next_req.buffers.items()))\n+\n+ self.textures[stream] = self.create_texture(stream, fb)\n+\n+ self.paint_gl()\n+\n+ def paint_gl(self):\n+ b = eglMakeCurrent(self.egl.display, self.surface, self.surface, self.egl.context)\n+ assert(b)\n+\n+ glClear(GL_COLOR_BUFFER_BIT)\n+\n+ size = self.size()\n+\n+ for idx, ctx in enumerate(self.state['contexts']):\n+ for stream in ctx['streams']:\n+ if self.textures[stream] is None:\n+ continue\n+\n+ w = size.width() // self.num_columns\n+ h = size.height() // self.num_rows\n+\n+ x = idx % self.num_columns\n+ y = idx // self.num_columns\n+\n+ x *= w\n+ y *= h\n+\n+ glViewport(x, y, w, h)\n+\n+ glBindTexture(GL_TEXTURE_EXTERNAL_OES, self.textures[stream])\n+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4)\n+\n+ b = eglSwapBuffers(self.egl.display, self.surface)\n+ assert(b)\n+\n+ def handle_request(self, ctx, req):\n+ self.reqqueue[ctx['idx']].append(req)\n+ self.update()\ndiff --git a/src/py/cam/gl_helpers.py b/src/py/cam/gl_helpers.py\nnew file mode 100644\nindex 00000000..ac5e6889\n--- /dev/null\n+++ b/src/py/cam/gl_helpers.py\n@@ -0,0 +1,74 @@\n+# SPDX-License-Identifier: GPL-2.0-or-later\n+# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n+\n+from OpenGL.EGL.VERSION.EGL_1_0 import EGLNativeDisplayType, eglGetProcAddress, eglQueryString, EGL_EXTENSIONS\n+\n+from OpenGL.raw.GLES2 import _types as _cs\n+from OpenGL.GLES2.VERSION.GLES2_2_0 import *\n+from OpenGL.GLES3.VERSION.GLES3_3_0 import *\n+from OpenGL import GL as gl\n+\n+from ctypes import c_int, c_char_p, c_void_p, cdll, POINTER, util, \\\n+ pointer, CFUNCTYPE, c_bool\n+\n+\n+def getEGLNativeDisplay():\n+ _x11lib = cdll.LoadLibrary(util.find_library('X11'))\n+ XOpenDisplay = _x11lib.XOpenDisplay\n+ XOpenDisplay.argtypes = [c_char_p]\n+ XOpenDisplay.restype = POINTER(EGLNativeDisplayType)\n+\n+ return XOpenDisplay(None)\n+\n+\n+# Hack. PyOpenGL doesn't seem to manage to find glEGLImageTargetTexture2DOES.\n+def getglEGLImageTargetTexture2DOES():\n+ funcptr = eglGetProcAddress('glEGLImageTargetTexture2DOES')\n+ prototype = CFUNCTYPE(None, _cs.GLenum, _cs.GLeglImageOES)\n+ return prototype(funcptr)\n+\n+\n+glEGLImageTargetTexture2DOES = getglEGLImageTargetTexture2DOES()\n+\n+# \\todo This can be dropped when we have proper PixelFormat bindings\n+def str_to_fourcc(str):\n+ assert(len(str) == 4)\n+ fourcc = 0\n+ for i, v in enumerate([ord(c) for c in str]):\n+ fourcc |= v << (i * 8)\n+ return fourcc\n+\n+\n+def get_gl_extensions():\n+ n = GLint()\n+ glGetIntegerv(GL_NUM_EXTENSIONS, n)\n+ gl_extensions = []\n+ for i in range(n.value):\n+ gl_extensions.append(gl.glGetStringi(GL_EXTENSIONS, i).decode())\n+ return gl_extensions\n+\n+\n+def check_gl_extensions(required_extensions):\n+ extensions = get_gl_extensions()\n+\n+ if False:\n+ print('GL EXTENSIONS: ', ' '.join(extensions))\n+\n+ for ext in required_extensions:\n+ if ext not in extensions:\n+ raise Exception(ext + ' missing')\n+\n+\n+def get_egl_extensions(egl_display):\n+ return eglQueryString(egl_display, EGL_EXTENSIONS).decode().split(' ')\n+\n+\n+def check_egl_extensions(egl_display, required_extensions):\n+ extensions = get_egl_extensions(egl_display)\n+\n+ if False:\n+ print('EGL EXTENSIONS: ', ' '.join(extensions))\n+\n+ for ext in required_extensions:\n+ if ext not in extensions:\n+ raise Exception(ext + ' missing')\n", "prefixes": [ "libcamera-devel", "v10", "7/7" ] }