Show a patch.

GET /api/patches/15790/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 15790,
    "url": "https://patchwork.libcamera.org/api/patches/15790/?format=api",
    "web_url": "https://patchwork.libcamera.org/patch/15790/",
    "project": {
        "id": 1,
        "url": "https://patchwork.libcamera.org/api/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": "<20220505104104.70841-8-tomi.valkeinen@ideasonboard.com>",
    "date": "2022-05-05T10:40:58",
    "name": "[libcamera-devel,v7,07/13] py: Add cam.py",
    "commit_ref": null,
    "pull_url": null,
    "state": "accepted",
    "archived": false,
    "hash": "b6c3f590dd374247d055ff1c030db8a868a7f7b3",
    "submitter": {
        "id": 109,
        "url": "https://patchwork.libcamera.org/api/people/109/?format=api",
        "name": "Tomi Valkeinen",
        "email": "tomi.valkeinen@ideasonboard.com"
    },
    "delegate": null,
    "mbox": "https://patchwork.libcamera.org/patch/15790/mbox/",
    "series": [
        {
            "id": 3093,
            "url": "https://patchwork.libcamera.org/api/series/3093/?format=api",
            "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=3093",
            "date": "2022-05-05T10:40:51",
            "name": "Python bindings",
            "version": 7,
            "mbox": "https://patchwork.libcamera.org/series/3093/mbox/"
        }
    ],
    "comments": "https://patchwork.libcamera.org/api/patches/15790/comments/",
    "check": "pending",
    "checks": "https://patchwork.libcamera.org/api/patches/15790/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 6BA59C326E\n\tfor <parsemail@patchwork.libcamera.org>;\n\tThu,  5 May 2022 10:41:36 +0000 (UTC)",
            "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 497B365669;\n\tThu,  5 May 2022 12:41:34 +0200 (CEST)",
            "from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 6E84665650\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu,  5 May 2022 12:41:29 +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 59915492;\n\tThu,  5 May 2022 12:41:28 +0200 (CEST)"
        ],
        "DKIM-Signature": [
            "v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1651747294;\n\tbh=u0GWr0XFBD34MHNnGfk2T5yscNS0W1+lRomnDScD0js=;\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=OUSZI+0QTHV2AjWw4Ep73LypYHyQ+BkIWEJNclMQXK5TPdDQW0pWEWtCZ9PyMP+a2\n\tiasLpVBArECPHt2TIn+b/Pg/YVpBh4pmrZD0X8jmNPnVpEqSbTLes9a6WaXG9e7qrA\n\tkStmCEkGoai7MjYNciIcM/67RKVkmRGuEEmWe2xvKSV/TxKWaxAkb3rlKYO5A43AIE\n\tD8PX788JW77suAjnppfCPmFtka5+5s45F71BMemrm7Vn5ZDkOmDiwcxZp5kcUx6kec\n\t0ejjlmiY72xCKBAr0s0/xcbNPvR4SnOaBx0LH6eTDF90Ho6hR+DYY/MH4wScWyEZdD\n\tXHeABEXgrAKcw==",
            "v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1651747289;\n\tbh=u0GWr0XFBD34MHNnGfk2T5yscNS0W1+lRomnDScD0js=;\n\th=From:To:Cc:Subject:Date:In-Reply-To:References:From;\n\tb=Wnme43OReqtcbbiWaS9x9AKHPJqAGV8gdD/T/92rqAAfL5wH2axlhxxJhHJuzbFYn\n\t3rxtApvwxdsO5uZ0D6sfY9iKc7SfYbB6foszGdmmq7GT4Z4e5ALjTYSTUz0XqltrRf\n\tKMtddYlJ7X1l3WiQirmHIk1mnQMHN/6n2bf0coME="
        ],
        "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (1024-bit key; \n\tunprotected) header.d=ideasonboard.com\n\theader.i=@ideasonboard.com\n\theader.b=\"Wnme43OR\"; 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": "Thu,  5 May 2022 13:40:58 +0300",
        "Message-Id": "<20220505104104.70841-8-tomi.valkeinen@ideasonboard.com>",
        "X-Mailer": "git-send-email 2.34.1",
        "In-Reply-To": "<20220505104104.70841-1-tomi.valkeinen@ideasonboard.com>",
        "References": "<20220505104104.70841-1-tomi.valkeinen@ideasonboard.com>",
        "MIME-Version": "1.0",
        "Content-Transfer-Encoding": "8bit",
        "Subject": "[libcamera-devel] [PATCH v7 07/13] 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>\n---\n src/py/cam/cam.py        | 483 +++++++++++++++++++++++++++++++++++++++\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   | 385 +++++++++++++++++++++++++++++++\n src/py/cam/gl_helpers.py |  74 ++++++\n 6 files changed, 1526 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..4efa6459\n--- /dev/null\n+++ b/src/py/cam/cam.py\n@@ -0,0 +1,483 @@\n+#!/usr/bin/env python3\n+\n+# SPDX-License-Identifier: GPL-2.0-or-later\n+# Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n+\n+import argparse\n+import binascii\n+import libcamera as libcam\n+import os\n+import sys\n+\n+\n+class CustomCameraAction(argparse.Action):\n+    def __call__(self, parser, namespace, values, option_string=None):\n+        print(self.dest, values)\n+\n+        if \"camera\" not in namespace or namespace.camera is None:\n+            setattr(namespace, \"camera\", [])\n+\n+        previous = namespace.camera\n+        previous.append((self.dest, values))\n+        setattr(namespace, \"camera\", previous)\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.generateConfiguration(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.toString()))\n+\n+        formats = stream_config.formats\n+        for fmt in formats.pixelFormats:\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.generateConfiguration(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.pixelFormat = stream_opts[\"pixelformat\"]\n+\n+    stat = camconfig.validate()\n+\n+    if stat == libcam.ConfigurationStatus.Invalid:\n+        print(\"Camera configuration invalid\")\n+        exit(-1)\n+    elif stat == libcam.ConfigurationStatus.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.toString()))\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(\"Can't 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.createRequest(ctx[\"idx\"])\n+\n+        if request is None:\n+            print(\"Can't 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.addBuffer(stream, buffer)\n+            if ret < 0:\n+                print(\"Can't 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.queueRequest(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.getReadyRequests()\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.RequestStatus.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(0) as b:\n+                crc = binascii.crc32(b)\n+                crcs.append(crc)\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(0) as b:\n+                filename = \"frame-{}-{}-{}.data\".format(ctx[\"id\"], stream_name, ctx[\"reqs-completed\"])\n+                with open(filename, \"wb\") as f:\n+                    f.write(b)\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.queueRequest(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..ee9fe6c7\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) 2021, 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.pixelFormat\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..f6e30835\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) 2021, 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..30fb7a1d\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) 2021, 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.pixelFormat\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(0) as b:\n+            cfg = stream.configuration\n+            w, h = cfg.size\n+            pitch = cfg.stride\n+\n+            if cfg.pixelFormat == \"MJPEG\":\n+                img = Image.open(BytesIO(b))\n+                qim = ImageQt(img).copy()\n+                pix = QtGui.QPixmap.fromImage(qim)\n+            else:\n+                data = np.array(b, dtype=np.uint8)\n+                rgb = to_rgb(cfg.pixelFormat, cfg.size, data)\n+\n+                if rgb is None:\n+                    raise Exception(\"Format not supported: \" + cfg.pixelFormat)\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..8f9ab457\n--- /dev/null\n+++ b/src/py/cam/cam_qtgl.py\n@@ -0,0 +1,385 @@\n+# SPDX-License-Identifier: GPL-2.0-or-later\n+# Copyright (C) 2021, 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(dpy, cfg, a):\n+                value = ctypes.c_long()\n+                eglGetConfigAttrib(dpy, cfg, a, value)\n+                return value.value\n+\n+            getconf = lambda a: _getconf(dpy, cfg, a)\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.pixelFormat\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.pixelFormat\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..925901dd\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) 2021, 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+    xdpy = 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+\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",
        "v7",
        "07/13"
    ]
}