[{"id":22903,"web_url":"https://patchwork.libcamera.org/comment/22903/","msgid":"<YnZw0aC/rd9uQzae@pendragon.ideasonboard.com>","date":"2022-05-07T13:14:57","subject":"Re: [libcamera-devel] [PATCH v9 7/7] py: Add cam.py","submitter":{"id":2,"url":"https://patchwork.libcamera.org/api/people/2/","name":"Laurent Pinchart","email":"laurent.pinchart@ideasonboard.com"},"content":"Hi Tomi,\n\nThank you for the patch.\n\nOn Sat, May 07, 2022 at 01:11:52PM +0300, Tomi Valkeinen wrote:\n> Add cam.py, which mimics the 'cam' tool. Four rendering backends are\n> added:\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> \n> All the renderers handle only a few pixel formats, and especially the GL\n> renderer is just a prototype.\n> \n> Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> ---\n>  src/py/cam/cam.py        | 472 +++++++++++++++++++++++++++++++++++++++\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, 1515 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\n> \n> diff --git a/src/py/cam/cam.py b/src/py/cam/cam.py\n> new file mode 100755\n> index 00000000..fc37089c\n> --- /dev/null\n> +++ b/src/py/cam/cam.py\n> @@ -0,0 +1,472 @@\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 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\nCould you please add a todo comment to remind that the following\nfunctions should be moved to a class ? Same for the state.\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())\n> diff --git a/src/py/cam/cam_kms.py b/src/py/cam/cam_kms.py\n> new file mode 100644\n> index 00000000..f4ee5a06\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.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)\n> diff --git a/src/py/cam/cam_null.py b/src/py/cam/cam_null.py\n> new file mode 100644\n> index 00000000..391397f6\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)\n> diff --git a/src/py/cam/cam_qt.py b/src/py/cam/cam_qt.py\n> new file mode 100644\n> index 00000000..40044866\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\nExtra space before comma. pycodestyle also warns about the space after [\nhere and two lines above, I don't mind keeping them for alignment, but\ngiven that the other values are not aligned anyway, I think we could\ndrop them.\n\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']))\n\npycodestyle reports\n\nsrc/py/cam/cam_qt.py:354:24: E128 continuation line under-indented for visual indent\n\n> diff --git a/src/py/cam/cam_qtgl.py b/src/py/cam/cam_qtgl.py\n> new file mode 100644\n> index 00000000..37b74d3f\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\nAnd here,\n\nsrc/py/cam/cam_qtgl.py:78:13: E731 do not assign a lambda expression, use a def\n\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()\n> diff --git a/src/py/cam/gl_helpers.py b/src/py/cam/gl_helpers.py\n> new file mode 100644\n> index 00000000..20a029cf\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> +    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> +\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\nShould we already add bindings for the PixelFormat class to handle this\nbetter ? It can be done on top too, with a comment reminding that this\nfunction should be dropped in favour of exposing PixelFormat in the\nPython bindings.\n\nWith those small issues addressed,\n\nReviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n\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')","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 84C48C0F2A\n\tfor <parsemail@patchwork.libcamera.org>;\n\tSat,  7 May 2022 13:15:04 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 91EA765645;\n\tSat,  7 May 2022 15:15:03 +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 C1D9C65643\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tSat,  7 May 2022 15:15:01 +0200 (CEST)","from pendragon.ideasonboard.com (62-78-145-57.bb.dnainternet.fi\n\t[62.78.145.57])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 96DFA55A;\n\tSat,  7 May 2022 15:15:00 +0200 (CEST)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1651929303;\n\tbh=yGpn61Sd+DYQJx6uRs2xJcJDFccQv90OrcunBZiS/bI=;\n\th=Date:To:References:In-Reply-To:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:Cc:\n\tFrom;\n\tb=Y0xjbh/OwkQGHu+MjNM24zfcAtEjX/J5/JT6Bkpd+GXX0N7r0rnoXaXXYUFFJtAVH\n\t1PUsGd136aqMzjdc0E8G445IQSkBFyFxtGmDsqCxd8tSYf1bhosVNcLP9B9cEIgKdN\n\tNFC9/MoHp3uABw+hDVI9x8r2Sk84nJUi2T7aPRO5Pt5ihPMk59tYs8Bjl8+F+PJczy\n\tuf3UUexDQMjzFAbbJGXRWVSQfU9HvCAZMKG9a5MwkIKAoqCoDe08SMY7XgqlWbwOzW\n\tWU820BZfvKuXFWongVemZ97ct/7o+qRaEkAypx024zOsVyhWKR3pvdK0vmdprieGPw\n\ttweVGjOCcnrpg==","v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1651929300;\n\tbh=yGpn61Sd+DYQJx6uRs2xJcJDFccQv90OrcunBZiS/bI=;\n\th=Date:From:To:Cc:Subject:References:In-Reply-To:From;\n\tb=n9E+iKigA6DV+sNmq8xgrIQYG9O+WXM/KdZl5iRDdXaOkPn6KKogGA87PY09udtfO\n\tEv0t9w6tatd88aWOOnqEHK7xX7SzpNcjsBAUGGNIkn73bpSoyRWm3jZoEc307I07+y\n\t7P/jfdKPtVz7dBysXcA4g7h1/KyRN3TYdyl9G1vw="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key; \n\tunprotected) header.d=ideasonboard.com\n\theader.i=@ideasonboard.com\n\theader.b=\"n9E+iKig\"; dkim-atps=neutral","Date":"Sat, 7 May 2022 16:14:57 +0300","To":"Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>","Message-ID":"<YnZw0aC/rd9uQzae@pendragon.ideasonboard.com>","References":"<20220507101152.31412-1-tomi.valkeinen@ideasonboard.com>\n\t<20220507101152.31412-8-tomi.valkeinen@ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","In-Reply-To":"<20220507101152.31412-8-tomi.valkeinen@ideasonboard.com>","Subject":"Re: [libcamera-devel] [PATCH v9 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":"Laurent Pinchart via libcamera-devel\n\t<libcamera-devel@lists.libcamera.org>","Reply-To":"Laurent Pinchart <laurent.pinchart@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}}]