From 5e737a984849f24e356f8d0104f969f5189defd7 Mon Sep 17 00:00:00 2001 From: Vasily Evseenko Date: Fri, 20 Dec 2024 15:51:52 +0300 Subject: [PATCH] Add rockchip libdrm rendering backend --- Makefile | 12 +- README.md | 12 +- drm_output.c | 1071 +++++++++++++++++++++++++++++++++++++++++++++++++ graphengine.c | 30 ++ main.c | 30 +- rpi_setup.sh | 2 +- 6 files changed, 1146 insertions(+), 11 deletions(-) create mode 100644 drm_output.c diff --git a/Makefile b/Makefile index 5a16ba5..24f1bae 100644 --- a/Makefile +++ b/Makefile @@ -21,10 +21,16 @@ ifeq ($(mode), gst) CFLAGS += -Wall -pthread -std=gnu99 -D__GST_OPENGL__ -fPIC $(shell pkg-config --cflags glib-2.0) $(shell pkg-config --cflags gstreamer-1.0) LDFLAGS += $(shell pkg-config --libs glib-2.0) $(shell pkg-config --libs gstreamer-1.0) $(shell pkg-config --libs gstreamer-video-1.0) -lgstapp-1.0 -lpthread -lrt -lm OBJS = main.o osdrender.o osdmavlink.o graphengine.o UAVObj.o m2dlib.o math3d.o osdconfig.o osdvar.o fonts.o font_outlined8x14.o font_outlined8x8.o appsrc.o gst-compat.o -else - CFLAGS += -Wall -pthread -std=gnu99 -D__BCM_OPENVG__ -I$(SYSROOT)/opt/vc/include/ -I$(SYSROOT)/opt/vc/include/interface/vcos/pthreads -I$(SYSROOT)/opt/vc/include/interface/vmcs_host/linux - LDFLAGS += -L$(SYSROOT)/opt/vc/lib/ -lbrcmGLESv2 -lbrcmEGL -lopenmaxil -lbcm_host -lvcos -lvchiq_arm -lpthread -lrt -lm +else ifeq ($(mode), rockchip) + CFLAGS += -Wall -pthread -std=gnu99 -D__DRM_ROCKCHIP__ -fPIC $(shell pkg-config --cflags libdrm) + LDFLAGS += $(shell pkg-config --libs libdrm) -lpthread -lrt -lm + OBJS = main.o osdrender.o osdmavlink.o graphengine.o UAVObj.o m2dlib.o math3d.o osdconfig.o osdvar.o fonts.o font_outlined8x14.o font_outlined8x8.o drm_output.o +else ifeq ($(mode), rpi3) + CFLAGS += -Wall -pthread -std=gnu99 -D__BCM_OPENVG__ -I/opt/vc/include/ -I/opt/vc/include/interface/vcos/pthreads -I/opt/vc/include/interface/vmcs_host/linux + LDFLAGS += -L/opt/vc/lib/ -lbrcmGLESv2 -lbrcmEGL -lopenmaxil -lbcm_host -lvcos -lvchiq_arm -lpthread -lrt -lm OBJS = main.o osdrender.o osdmavlink.o graphengine.o UAVObj.o m2dlib.o math3d.o osdconfig.o osdvar.o fonts.o font_outlined8x14.o font_outlined8x8.o oglinit.o +else + $(error Valid modes are: gst, rockchip or rpi3) endif all: osd diff --git a/README.md b/README.md index 7cfec16..7e051dc 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,9 @@ This project started from https://github.com/TobiasBales/PlayuavOSD.git Supported platforms: ------------------- * Raspberry Pi 0-3 -- use hardware overlay mode (OpenVG) - * Any other Linux with X11 or Wayland -- use GStreamer OpenGL mixer + * Radxa Zero 3W/3E -- use hardware overlay mode (libdrm) + * OrangePi 5 -- use hardware overlay mode (libdrm) + * Any other Linux with X11/Wayland and GPU -- use GStreamer OpenGL mixer Supported autopilots: --------------------- @@ -16,11 +18,15 @@ Building: --------- 1. Build for Linux (X11 or Wayland) (native build): - * `apt-get install gstreamer1.0-tools libgstreamer1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-bad gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly libgstreamer-plugins-base1.0-dev` + * `apt-get install gstreamer1.0-tools pkg-config libgstreamer1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-bad gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly libgstreamer-plugins-base1.0-dev` * `make osd` 2. Build for Raspberry PI 0-3 (OpenVG) (native build): - * `make osd mode=rpi` + * `make osd mode=rpi3` + +3. Build for Radxa or OrangePi (libdrm) (native build): + * `apt-get install libdrm-dev pkg-config` + * `make osd mode=rockchip` Running: -------- diff --git a/drm_output.c b/drm_output.c new file mode 100644 index 0000000..41c101e --- /dev/null +++ b/drm_output.c @@ -0,0 +1,1071 @@ +/* + Copyright (C) 2024 Vasily Evseenko + + Based on example written by David Rheinsberg + http://github.com/dvdhrm/docs + + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + + +/* + * Planes can be used to blend or overlay images on top of a CRTC + * framebuffer during the scanout process. Not all hardware provide + * planes and the number of planes available is also limited. If there's + * not enough planes available or the hardware does not provide them, + * users should fallback to composition via GPU or CPU to blend or + * overlay the planes. Notice that this render process will result + * in delay, what justifies the usage of planes by modern hardware + * that needs to be fast. + * + * There are three types of planes: primary, cursor and overlay. For + * compatibility with legacy userspace, the default behavior is to expose + * only overlay planes to userspace (we're going to see in the code that + * we have to ask to receive all types of planes). A good example of plane + * usage is this: imagine a static desktop screen and the user is moving + * the cursor around. Only the cursor is moving. Instead of calculating the + * complete scene for each time the user moves its cursor, we can update only + * the cursor plane and it will be automatically overlayed by the hardware on + * top of the primary plane. There's no need of software composition in this + * case. + * + * But there was synchronisation problems related to multiple planes + * usage. The KMS API was not atomic, so you'd have to update the primary + * plane and then the overlay planes with distinct IOCTL's. This could lead + * to tearing and also some trouble related to blocking, so the atomic + * API was proposed to fix these problems. + * + * With the introduction of the KMS atomic API, all the planes could get + * updated in a single IOCTL, using drmModeAtomicCommit(). This can be + * either asynchronous or fully blocking. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "graphengine.h" + +#define FB_WIDTH GRAPHICS_WIDTH +#define FB_HEIGHT GRAPHICS_HEIGHT +#define ZPOS 7 + +/* + * A new struct is introduced: drm_object. It stores properties of certain + * objects (connectors, CRTC and planes) that are used in atomic modeset setup + * and also in atomic page-flips (all planes updated in a single IOCTL). + */ + +struct drm_object { + drmModeObjectProperties *props; + drmModePropertyRes **props_info; + uint32_t id; +}; + +struct modeset_buf { + uint32_t width; + uint32_t height; + uint32_t stride; + uint32_t size; + uint32_t handle; + uint8_t *map; + uint32_t fb; +}; + +struct modeset_output { + struct modeset_output *next; + + unsigned int front_buf; + struct modeset_buf bufs[2]; + + struct drm_object connector; + struct drm_object crtc; + struct drm_object plane; + + drmModeModeInfo mode; + uint32_t mode_blob_id; + uint32_t crtc_index; +}; + +static struct modeset_output *output_list = NULL; + +/* + * modeset_open() changes just a little bit. We now have to set that we're going + * to use the KMS atomic API and check if the device is capable of handling it. + */ + +static int modeset_open(int *out, const char *node) +{ + int fd, ret; + uint64_t cap; + + fd = open(node, O_RDWR | O_CLOEXEC); + if (fd < 0) { + ret = -errno; + fprintf(stderr, "cannot open '%s': %m\n", node); + return ret; + } + + /* Set that we want to receive all the types of planes in the list. This + * have to be done since, for legacy reasons, the default behavior is to + * expose only the overlay planes to the users. The atomic API only + * works if this is set. + */ + ret = drmSetClientCap(fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1); + if (ret) { + fprintf(stderr, "failed to set universal planes cap, %d\n", ret); + return ret; + } + + /* Here we set that we're going to use the KMS atomic API. It's supposed + * to set the DRM_CLIENT_CAP_UNIVERSAL_PLANES automatically, but it's a + * safe behavior to set it explicitly as we did in the previous + * commands. This is also good for learning purposes. + */ + ret = drmSetClientCap(fd, DRM_CLIENT_CAP_ATOMIC, 1); + if (ret) { + fprintf(stderr, "failed to set atomic cap, %d", ret); + return ret; + } + + if (drmGetCap(fd, DRM_CAP_DUMB_BUFFER, &cap) < 0 || !cap) { + fprintf(stderr, "drm device '%s' does not support dumb buffers\n", + node); + close(fd); + return -EOPNOTSUPP; + } + + if (drmGetCap(fd, DRM_CAP_CRTC_IN_VBLANK_EVENT, &cap) < 0 || !cap) { + fprintf(stderr, "drm device '%s' does not support atomic KMS\n", + node); + close(fd); + return -EOPNOTSUPP; + } + + *out = fd; + return 0; +} + +/* + * get_property_value() is a new function. Given a device, the properties of + * an object and a name, search for the value of property 'name'. If we can't + * find it, return -1. + */ + +static int64_t get_property_value(int fd, drmModeObjectPropertiesPtr props, + const char *name) +{ + drmModePropertyPtr prop; + uint64_t value; + bool found; + int j; + + found = false; + for (j = 0; j < props->count_props && !found; j++) { + prop = drmModeGetProperty(fd, props->props[j]); + if (!strcmp(prop->name, name)) { + value = props->prop_values[j]; + found = true; + } + drmModeFreeProperty(prop); + } + + if (!found) + return -1; + return value; +} + +/* + * get_drm_object_properties() is a new helpfer function that retrieves + * the properties of a certain CRTC, plane or connector object. + */ + +static void modeset_get_object_properties(int fd, struct drm_object *obj, + uint32_t type) +{ + const char *type_str; + unsigned int i; + + obj->props = drmModeObjectGetProperties(fd, obj->id, type); + if (!obj->props) { + switch(type) { + case DRM_MODE_OBJECT_CONNECTOR: + type_str = "connector"; + break; + case DRM_MODE_OBJECT_PLANE: + type_str = "plane"; + break; + case DRM_MODE_OBJECT_CRTC: + type_str = "CRTC"; + break; + default: + type_str = "unknown type"; + break; + } + fprintf(stderr, "cannot get %s %d properties: %s\n", + type_str, obj->id, strerror(errno)); + return; + } + + obj->props_info = calloc(obj->props->count_props, sizeof(obj->props_info)); + for (i = 0; i < obj->props->count_props; i++) + obj->props_info[i] = drmModeGetProperty(fd, obj->props->props[i]); +} + +/* + * set_drm_object_property() is a new function. It sets a property value to a + * CRTC, plane or connector object. + */ + +static int set_drm_object_property(drmModeAtomicReq *req, struct drm_object *obj, + const char *name, uint64_t value) +{ + int i; + uint32_t prop_id = 0; + + for (i = 0; i < obj->props->count_props; i++) { + if (!strcmp(obj->props_info[i]->name, name)) { + prop_id = obj->props_info[i]->prop_id; + break; + } + } + + if (prop_id == 0) { + fprintf(stderr, "no object property: %s\n", name); + return -EINVAL; + } + + return drmModeAtomicAddProperty(req, obj->id, prop_id, value); +} + +/* + * modeset_find_crtc() changes a little bit. Now we also have to save the CRTC + * index, and not only its id. + */ + +static int modeset_find_crtc(int fd, drmModeRes *res, drmModeConnector *conn, + struct modeset_output *out) +{ + drmModeEncoder *enc; + unsigned int i, j; + uint32_t crtc; + struct modeset_output *iter; + + /* first try the currently conected encoder+crtc */ + if (conn->encoder_id) + enc = drmModeGetEncoder(fd, conn->encoder_id); + else + enc = NULL; + + if (enc) { + if (enc->crtc_id) { + crtc = enc->crtc_id; + for (iter = output_list; iter; iter = iter->next) { + if (iter->crtc.id == crtc) { + crtc = 0; + break; + } + } + + if (crtc > 0) { + drmModeFreeEncoder(enc); + out->crtc.id = crtc; + /* find the CRTC's index */ + for (i = 0; i < res->count_crtcs; ++i) { + if (res->crtcs[i] == crtc) { + out->crtc_index = i; + break; + } + } + return 0; + } + } + + drmModeFreeEncoder(enc); + } + + /* If the connector is not currently bound to an encoder or if the + * encoder+crtc is already used by another connector (actually unlikely + * but lets be safe), iterate all other available encoders to find a + * matching CRTC. + */ + for (i = 0; i < conn->count_encoders; ++i) { + enc = drmModeGetEncoder(fd, conn->encoders[i]); + if (!enc) { + fprintf(stderr, "cannot retrieve encoder %u:%u (%d): %m\n", + i, conn->encoders[i], errno); + continue; + } + + /* iterate all global CRTCs */ + for (j = 0; j < res->count_crtcs; ++j) { + /* check whether this CRTC works with the encoder */ + if (!(enc->possible_crtcs & (1 << j))) + continue; + + /* check that no other output already uses this CRTC */ + crtc = res->crtcs[j]; + for (iter = output_list; iter; iter = iter->next) { + if (iter->crtc.id == crtc) { + crtc = 0; + break; + } + } + + /* We have found a CRTC, so save it and return. Note + * that we have to save its index as well. The CRTC + * index (not its ID) will be used when searching for a + * suitable plane. + */ + if (crtc > 0) { + fprintf(stdout, "crtc %u found for encoder %u, will need full modeset\n", + crtc, conn->encoders[i]);; + drmModeFreeEncoder(enc); + out->crtc.id = crtc; + out->crtc_index = j; + return 0; + } + } + + drmModeFreeEncoder(enc); + } + + fprintf(stderr, "cannot find suitable crtc for connector %u\n", + conn->connector_id); + return -ENOENT; +} + +/* + * modeset_find_plane() is a new function. Given a certain combination + * of connector+CRTC, it looks for a primary plane for it. + */ + +static int modeset_find_plane(int fd, struct modeset_output *out) +{ + drmModePlaneResPtr plane_res; + bool found_primary = false; + int i, ret = -EINVAL; + + plane_res = drmModeGetPlaneResources(fd); + if (!plane_res) { + fprintf(stderr, "drmModeGetPlaneResources failed: %s\n", + strerror(errno)); + return -ENOENT; + } + + /* iterates through all planes of a certain device */ + for (i = 0; (i < plane_res->count_planes) && !found_primary; i++) { + int plane_id = plane_res->planes[i]; + + drmModePlanePtr plane = drmModeGetPlane(fd, plane_id); + if (!plane) { + fprintf(stderr, "drmModeGetPlane(%u) failed: %s\n", plane_id, + strerror(errno)); + continue; + } + + /* check if the plane can be used by our CRTC */ + if (plane->possible_crtcs & (1 << out->crtc_index)) { + drmModeObjectPropertiesPtr props = + drmModeObjectGetProperties(fd, plane_id, DRM_MODE_OBJECT_PLANE); + + /* Get the "type" property to check if this is a primary + * plane. Type property is special, as its enum value is + * defined in UAPI headers. For the properties that are + * not defined in the UAPI headers, we would have to + * give kernel the property name and it would return the + * corresponding enum value. We could also do this for + * the "type" property, but it would make this simple + * example more complex. The reason why defining enum + * values for kernel properties in UAPI headers is + * deprecated is that string names are easier to both + * (userspace and kernel) make unique and keep + * consistent between drivers and kernel versions. But + * in order to not break userspace, some properties were + * left in the UAPI headers as well. + */ + if (get_property_value(fd, props, "type") == DRM_PLANE_TYPE_PRIMARY) { + found_primary = true; + out->plane.id = plane_id; + ret = 0; + } + + drmModeFreeObjectProperties(props); + } + + drmModeFreePlane(plane); + } + + drmModeFreePlaneResources(plane_res); + + if (found_primary) + fprintf(stdout, "found primary plane, id: %d\n", out->plane.id); + else + fprintf(stdout, "couldn't find a primary plane\n"); + return ret; +} + +/* + * modeset_drm_object_fini() is a new helper function that destroys CRTCs, + * connectors and planes + */ + +static void modeset_drm_object_fini(struct drm_object *obj) +{ + for (int i = 0; i < obj->props->count_props; i++) + drmModeFreeProperty(obj->props_info[i]); + free(obj->props_info); + drmModeFreeObjectProperties(obj->props); +} + +/* + * modeset_setup_objects() is a new function. It helps us to retrieve + * connector, CRTC and plane objects properties from the device. These + * properties will help us during the atomic modesetting commit, so we save + * them in our struct modeset_output object. + */ + +static int modeset_setup_objects(int fd, struct modeset_output *out) +{ + struct drm_object *connector = &out->connector; + struct drm_object *crtc = &out->crtc; + struct drm_object *plane = &out->plane; + + /* retrieve connector properties from the device */ + modeset_get_object_properties(fd, connector, DRM_MODE_OBJECT_CONNECTOR); + if (!connector->props) + goto out_conn; + + /* retrieve CRTC properties from the device */ + modeset_get_object_properties(fd, crtc, DRM_MODE_OBJECT_CRTC); + if (!crtc->props) + goto out_crtc; + + /* retrieve plane properties from the device */ + modeset_get_object_properties(fd, plane, DRM_MODE_OBJECT_PLANE); + if (!plane->props) + goto out_plane; + + return 0; + +out_plane: + modeset_drm_object_fini(crtc); +out_crtc: + modeset_drm_object_fini(connector); +out_conn: + return -ENOMEM; +} + +/* + * modeset_destroy_objects() is a new function. It destroys what we allocate + * in modeset_setup_objects(). + */ + +static void modeset_destroy_objects(int fd, struct modeset_output *out) +{ + modeset_drm_object_fini(&out->connector); + modeset_drm_object_fini(&out->crtc); + modeset_drm_object_fini(&out->plane); +} + +/* + * modeset_create_fb() stays the same. + */ + +static int modeset_create_fb(int fd, struct modeset_buf *buf) +{ + struct drm_mode_create_dumb creq; + struct drm_mode_destroy_dumb dreq; + struct drm_mode_map_dumb mreq; + int ret; + uint32_t handles[4] = {0}, pitches[4] = {0}, offsets[4] = {0}; + + /* create dumb buffer */ + memset(&creq, 0, sizeof(creq)); + creq.width = buf->width; + creq.height = buf->height; + creq.bpp = 32; + ret = drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &creq); + if (ret < 0) { + fprintf(stderr, "cannot create dumb buffer (%d): %m\n", + errno); + return -errno; + } + buf->stride = creq.pitch; + buf->size = creq.size; + buf->handle = creq.handle; + + /* create framebuffer object for the dumb-buffer */ + handles[0] = buf->handle; + pitches[0] = buf->stride; + ret = drmModeAddFB2(fd, buf->width, buf->height, DRM_FORMAT_ABGR8888, + handles, pitches, offsets, &buf->fb, 0); + if (ret) { + fprintf(stderr, "cannot create framebuffer (%d): %m\n", + errno); + ret = -errno; + goto err_destroy; + } + + /* prepare buffer for memory mapping */ + memset(&mreq, 0, sizeof(mreq)); + mreq.handle = buf->handle; + ret = drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &mreq); + if (ret) { + fprintf(stderr, "cannot map dumb buffer (%d): %m\n", + errno); + ret = -errno; + goto err_fb; + } + + /* perform actual memory mapping */ + buf->map = mmap(0, buf->size, PROT_READ | PROT_WRITE, MAP_SHARED, + fd, mreq.offset); + if (buf->map == MAP_FAILED) { + fprintf(stderr, "cannot mmap dumb buffer (%d): %m\n", + errno); + ret = -errno; + goto err_fb; + } + + /* clear the framebuffer to 0 */ + memset(buf->map, 0, buf->size); + + return 0; + +err_fb: + drmModeRmFB(fd, buf->fb); +err_destroy: + memset(&dreq, 0, sizeof(dreq)); + dreq.handle = buf->handle; + drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &dreq); + return ret; +} + +/* + * modeset_destroy_fb() stays the same. + */ + +static void modeset_destroy_fb(int fd, struct modeset_buf *buf) +{ + struct drm_mode_destroy_dumb dreq; + + /* unmap buffer */ + munmap(buf->map, buf->size); + + /* delete framebuffer */ + drmModeRmFB(fd, buf->fb); + + /* delete dumb buffer */ + memset(&dreq, 0, sizeof(dreq)); + dreq.handle = buf->handle; + drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &dreq); +} + +/* + * modeset_setup_framebuffers() creates framebuffers for the back and front + * buffers of a certain output. Also, it copies the connector mode to these + * buffers. + */ + +static int modeset_setup_framebuffers(int fd, + struct modeset_output *out) +{ + int i, ret; + + /* setup the front and back framebuffers */ + for (i = 0; i < 2; i++) { + + /* copy mode info to buffer */ + out->bufs[i].width = FB_WIDTH; + out->bufs[i].height = FB_HEIGHT; + + /* create a framebuffer for the buffer */ + ret = modeset_create_fb(fd, &out->bufs[i]); + if (ret) { + /* the second framebuffer creation failed, so + * we have to destroy the first before returning */ + if (i == 1) + modeset_destroy_fb(fd, &out->bufs[0]); + return ret; + } + } + + return 0; +} + +/* + * modeset_output_destroy() is new. It destroys the objects (connector, crtc and + * plane), front and back buffers, the mode blob property and then destroys the + * output itself. + */ + +static void modeset_output_destroy(int fd, struct modeset_output *out) +{ + /* destroy connector, crtc and plane objects */ + modeset_destroy_objects(fd, out); + + /* destroy front/back framebuffers */ + modeset_destroy_fb(fd, &out->bufs[0]); + modeset_destroy_fb(fd, &out->bufs[1]); + + /* destroy mode blob property */ + drmModeDestroyPropertyBlob(fd, out->mode_blob_id); + + free(out); +} + +/* + * With a certain combination of connector+CRTC, we look for a suitable primary + * plane for it. After that, we retrieve connector, CRTC and plane objects + * properties from the device. These objects are used during the atomic modeset + * setup (see modeset_atomic_prepare_commit()) and also during the page-flips + * (see modeset_draw_commit() and modeset_atomic_commit()). + * + * Besides that, we have to create a blob property that receives the output + * mode. When we perform an atomic commit, the driver expects a CRTC property + * named "MODE_ID", which points to the id of a blob. This usually happens for + * properties that are not simple types. In this particular case, out->mode is a + * struct. But we could have another property that expects the id of a blob that + * holds an array, for instance. + */ + +static struct modeset_output *modeset_output_create(int fd, drmModeRes *res, + drmModeConnector *conn) +{ + int ret; + struct modeset_output *out; + + /* creates an output structure */ + out = malloc(sizeof(*out)); + memset(out, 0, sizeof(*out)); + out->connector.id = conn->connector_id; + + /* check if a monitor is connected */ + if (conn->connection != DRM_MODE_CONNECTED) { + fprintf(stderr, "ignoring unused connector %u\n", + conn->connector_id); + goto out_error; + } + + /* check if there is at least one valid mode */ + if (conn->count_modes == 0) { + fprintf(stderr, "no valid mode for connector %u\n", + conn->connector_id); + goto out_error; + } + + /* copy the mode information into our output structure */ + memcpy(&out->mode, &conn->modes[0], sizeof(out->mode)); + /* create the blob property using out->mode and save its id in the output*/ + if (drmModeCreatePropertyBlob(fd, &out->mode, sizeof(out->mode), + &out->mode_blob_id) != 0) { + fprintf(stderr, "couldn't create a blob property\n"); + goto out_error; + } + fprintf(stderr, "mode for connector %u is %ux%u\n", + conn->connector_id, out->bufs[0].width, out->bufs[0].height); + + /* find a crtc for this connector */ + ret = modeset_find_crtc(fd, res, conn, out); + if (ret) { + fprintf(stderr, "no valid crtc for connector %u\n", + conn->connector_id); + goto out_blob; + } + + /* with a connector and crtc, find a primary plane */ + ret = modeset_find_plane(fd, out); + if (ret) { + fprintf(stderr, "no valid plane for crtc %u\n", out->crtc.id); + goto out_blob; + } + + /* gather properties of our connector, CRTC and planes */ + ret = modeset_setup_objects(fd, out); + if (ret) { + fprintf(stderr, "cannot get plane properties\n"); + goto out_blob; + } + + /* setup front/back framebuffers for this CRTC */ + ret = modeset_setup_framebuffers(fd, out); + if (ret) { + fprintf(stderr, "cannot create framebuffers for connector %u\n", + conn->connector_id); + goto out_obj; + } + + return out; + +out_obj: + modeset_destroy_objects(fd, out); +out_blob: + drmModeDestroyPropertyBlob(fd, out->mode_blob_id); +out_error: + free(out); + return NULL; +} + +/* + * modeset_prepare() changes a little bit. Now we use the new function + * modeset_output_create() to allocate memory and setup the output. + */ + +static int modeset_prepare(int fd) +{ + drmModeRes *res; + drmModeConnector *conn; + unsigned int i; + struct modeset_output *out; + + /* retrieve resources */ + res = drmModeGetResources(fd); + if (!res) { + fprintf(stderr, "cannot retrieve DRM resources (%d): %m\n", + errno); + return -errno; + } + + /* iterate all connectors */ + for (i = 0; i < res->count_connectors; ++i) { + /* get information for each connector */ + conn = drmModeGetConnector(fd, res->connectors[i]); + if (!conn) { + fprintf(stderr, "cannot retrieve DRM connector %u:%u (%d): %m\n", + i, res->connectors[i], errno); + continue; + } + + /* create an output structure and free connector data */ + out = modeset_output_create(fd, res, conn); + drmModeFreeConnector(conn); + if (!out) + continue; + + /* link output into global list */ + out->next = output_list; + output_list = out; + } + if (!output_list) { + fprintf(stderr, "couldn't create any outputs\n"); + return -1; + } + + /* free resources again */ + drmModeFreeResources(res); + return 0; +} + +/* + * modeset_atomic_prepare_commit() is new. Here we set the values of properties + * (of our connector, CRTC and plane objects) that we want to change in the + * atomic commit. These changes are temporarily stored in drmModeAtomicReq *req + * until the commit actually happens. + */ + +static int modeset_atomic_prepare_commit(int fd, struct modeset_output *out, + drmModeAtomicReq *req) +{ + struct drm_object *plane = &out->plane; + struct modeset_buf *buf = &out->bufs[out->front_buf ^ 1]; + + /* set id of the CRTC id that the connector is using */ + if (set_drm_object_property(req, &out->connector, "CRTC_ID", out->crtc.id) < 0) + return -1; + + /* set the mode id of the CRTC; this property receives the id of a blob + * property that holds the struct that actually contains the mode info */ + if (set_drm_object_property(req, &out->crtc, "MODE_ID", out->mode_blob_id) < 0) + return -1; + + /* set the CRTC object as active */ + if (set_drm_object_property(req, &out->crtc, "ACTIVE", 1) < 0) + return -1; + + /* set properties of the plane related to the CRTC and the framebuffer */ + if (set_drm_object_property(req, plane, "FB_ID", buf->fb) < 0) + return -1; + if (set_drm_object_property(req, plane, "CRTC_ID", out->crtc.id) < 0) + return -1; + if (set_drm_object_property(req, plane, "SRC_X", 0) < 0) + return -1; + if (set_drm_object_property(req, plane, "SRC_Y", 0) < 0) + return -1; + if (set_drm_object_property(req, plane, "SRC_W", buf->width << 16) < 0) + return -1; + if (set_drm_object_property(req, plane, "SRC_H", buf->height << 16) < 0) + return -1; + if (set_drm_object_property(req, plane, "CRTC_X", 0) < 0) + return -1; + if (set_drm_object_property(req, plane, "CRTC_Y", 0) < 0) + return -1; + if (set_drm_object_property(req, plane, "CRTC_W", out->mode.hdisplay) < 0) + return -1; + if (set_drm_object_property(req, plane, "CRTC_H", out->mode.vdisplay) < 0) + return -1; + + return 0; +} + +/* + * Draw on back framebuffer before the page-flip is requested. + */ + +static void modeset_paint_framebuffer(struct modeset_output *out, uint32_t color) +{ + struct modeset_buf *buf; + + buf = &out->bufs[out->front_buf ^ 1]; + for (int j = 0; j < buf->height; ++j) { + for (int k = 0; k < buf->width; ++k) { + int off = buf->stride * j + k * 4; + *(uint32_t*)(buf->map + off) = color; + } + } +} + +/* + * modeset_draw_commit() prepares the framebuffer with the drawing and then it asks + * for the driver to perform an atomic commit. This will lead to a page-flip and + * the content of the framebuffer will be displayed. In this simple example + * we're only using the primary plane, but we could also be updating other + * planes in the same atomic commit. + * + * Just like in modeset_perform_modeset(), we first setup everything with + * modeset_atomic_prepare_commit() and then actually perform the atomic commit. + * But there are some important differences: + * + * 1. Here we just want to perform a commit that changes the state of a specific + * output, and in modeset_perform_modeset() we did an atomic commit that was + * supposed to setup all the outputs at once. So there's no need to prepare + * every output before performing the atomic commit. But let's suppose you + * prepare every output and then perform the commit. It should schedule a + * page-flip for all of them, but modeset_draw_commit() was called because the + * page-flip for a specific output has finished. The others may not be + * prepared for a page-flip yet (e.g. in the middle of a scanout), so these + * page-flips will fail. + * + * 2. Here we have already painted the framebuffer and also we don't use the + * flag DRM_MODE_ALLOW_MODESET anymore, since the modeset already happened. + * We could continue to use this flag, as it makes no difference if + * modeset_perform_modeset() is correct and there's no bug in the kernel. + * The flag only allows (it doesn't force) the driver to perform a modeset, + * but we have already performed it in modeset_perform_modeset() and now we + * just want page-flips to occur. If we still need to perform modesets it + * means that we have a bug somewhere, and it may be better to fail than to + * glitch (a modeset can cause unecessary latency and also blank the screen). + */ + +static void modeset_draw_commit(int fd, struct modeset_output *out) +{ + drmModeAtomicReq *req; + int ret, flags; + + /* prepare output for atomic commit */ + req = drmModeAtomicAlloc(); + ret = modeset_atomic_prepare_commit(fd, out, req); + if (ret < 0) { + fprintf(stderr, "prepare atomic commit failed, %d\n", errno); + return; + } + + /* We've just draw on the framebuffer, prepared the commit and now it's + * time to perform a page-flip to display its content. + * + * DRM_MODE_ATOMIC_NONBLOCK makes the page-flip non-blocking. We don't + * want to be blocked waiting for the commit to happen, since we can use + * this time to prepare a new framebuffer, for instance. We can only do + * this because there are mechanisms to know when the commit is complete + * (like page flip event, explained above). + */ + flags = DRM_MODE_ATOMIC_NONBLOCK; + ret = drmModeAtomicCommit(fd, req, flags, NULL); + drmModeAtomicFree(req); + + if (ret < 0) { + fprintf(stderr, "atomic commit failed, %d\n", errno); + return; + } + + out->front_buf ^= 1; +} + + +/* + * modeset_perform_modeset() is new. First we define what properties have to be + * changed and the values that they will receive. To check if the modeset will + * work as expected, we perform an atomic commit with the flag + * DRM_MODE_ATOMIC_TEST_ONLY. With this flag the DRM driver tests if the atomic + * commit would work, but it doesn't commit it to the hardware. After, the same + * atomic commit is performed without the TEST_ONLY flag, but not only before we + * draw on the framebuffers of the outputs. This is necessary to avoid + * displaying unwanted content. + * + * NOTE: we can't perform an atomic commit without an attached frambeuffer + * (even when we have DRM_MODE_ATOMIC_TEST_ONLY). It will simply fail. + */ + +static int modeset_perform_modeset(int fd) +{ + int ret, flags; + struct modeset_output *iter; + drmModeAtomicReq *req; + + /* prepare modeset on all outputs */ + req = drmModeAtomicAlloc(); + for (iter = output_list; iter; iter = iter->next) { + ret = modeset_atomic_prepare_commit(fd, iter, req); + if (ret < 0) + break; + } + if (ret < 0) { + fprintf(stderr, "prepare atomic commit failed, %d\n", errno); + return ret; + } + + /* perform test-only atomic commit */ + flags = DRM_MODE_ATOMIC_TEST_ONLY | DRM_MODE_ATOMIC_ALLOW_MODESET; + ret = drmModeAtomicCommit(fd, req, flags, NULL); + if (ret < 0) { + fprintf(stderr, "test-only atomic commit failed, %d\n", errno); + drmModeAtomicFree(req); + return ret; + } + + /* draw on back framebuffer of all outputs */ + for (iter = output_list; iter; iter = iter->next) + { + if (set_drm_object_property(req, &iter->plane, "zpos", ZPOS) < 0) + { + fprintf(stderr, "Unable to set zpos %d for primary plane\n", ZPOS); + drmModeAtomicFree(req); + return -1; + } + modeset_paint_framebuffer(iter, 0); + } + + /* initial modeset on all outputs */ + flags = DRM_MODE_ATOMIC_ALLOW_MODESET; + ret = drmModeAtomicCommit(fd, req, flags, NULL); + if (ret < 0) + { + fprintf(stderr, "modeset atomic commit failed, %d\n", errno); + } + + drmModeAtomicFree(req); + + return ret; +} + +static void modeset_cleanup(int fd) +{ + struct modeset_output *iter; + drmModeAtomicReq *req; + int ret = 0; + + /* restore zpos on all outputs */ + req = drmModeAtomicAlloc(); + + for(iter = output_list; iter; iter = iter->next) + { + set_drm_object_property(req, &iter->plane, "zpos", 0); + } + + ret = drmModeAtomicCommit(fd, req, 0, NULL); + + if (ret < 0) + { + fprintf(stderr, "Unable to restore zpos for primary plane\n"); + } + + drmModeAtomicFree(req); + + while (output_list) + { + /* get first output from list */ + iter = output_list; + + /* move head of the list to the next output */ + output_list = iter->next; + + /* destroy current output */ + modeset_output_destroy(fd, iter); + } +} + +static int drm_fd = -1; + +void drm_cleanup(void) +{ + /* cleanup everything */ + modeset_cleanup(drm_fd); + close(drm_fd); +} + +int drm_init(void) +{ + int ret; + const char *card = "/dev/dri/card0"; + + fprintf(stderr, "DRM using card '%s'\n", card); + + /* open the DRM device */ + ret = modeset_open(&drm_fd, card); + if (ret) + goto out_return; + + /* prepare all connectors and CRTCs */ + ret = modeset_prepare(drm_fd); + if (ret) + goto out_close; + + modeset_perform_modeset(drm_fd); + + return 0; + +out_close: + close(drm_fd); +out_return: + if (ret) { + errno = -ret; + fprintf(stderr, "modeset failed with error %d: %m\n", errno); + } else { + fprintf(stderr, "exiting\n"); + } + return ret; +} + + +void drm_display_buffer(void *src_buf) +{ + for (struct modeset_output *iter = output_list; iter; iter = iter->next) + { + struct modeset_buf *dst_buf = &iter->bufs[iter->front_buf ^ 1]; + memcpy(dst_buf->map, src_buf, dst_buf->size); + modeset_draw_commit(drm_fd, iter); + } +} diff --git a/graphengine.c b/graphengine.c index 3b53708..4b0676e 100644 --- a/graphengine.c +++ b/graphengine.c @@ -143,6 +143,36 @@ void *displayGraphics(void) #endif +#ifdef __DRM_ROCKCHIP__ + +int drm_init(void); +void drm_cleanup(void); +void drm_display_buffer(void *src_buf); + +void render_init(int shift_x, int shift_y, float scale_x, float scale_y) +{ + if(drm_init() != 0) + { + exit(1); + } + atexit(drm_cleanup); + video_buf_int = malloc(GRAPHICS_WIDTH * GRAPHICS_HEIGHT * 4); +} + +void clearGraphics(void) +{ + memset(video_buf_int, '\0', GRAPHICS_WIDTH * GRAPHICS_HEIGHT * 4); +} + +void* displayGraphics(void) +{ + drm_display_buffer(video_buf_int); + return NULL; +} + +#endif + + void* render(void) { clearGraphics(); diff --git a/main.c b/main.c index 945934f..0d44c09 100644 --- a/main.c +++ b/main.c @@ -50,8 +50,14 @@ int gst_main(int rtp_port, char *codec, int rtp_jitter, osd_render_t osd_render, int screen_width, char *rtsp_url); #endif +static volatile uint8_t finished = 0; int osd_debug = 0; +void sigterm_handler(int signum) +{ + finished = 1; +} + int open_udp_socket_for_rx(int port) { struct sockaddr_in saddr; @@ -139,12 +145,17 @@ int main(int argc, char **argv) case 'h': default: show_usage: + +#ifdef __GST_OPENGL__ fprintf(stderr, "%s [-p mavlink_port] [-P rtp_port] [ -R rtsp_url ] [-4] [-5] [-j rtp_jitter] [-x] [-a] [-w screen_width] \n", argv[0]); fprintf(stderr, "Default: mavlink_port=%d, rtp_port=%d, rtsp_url=%s, codec=%s, rtp_jitter=%d, screen_width=%d\n", osd_port, rtp_port, rtsp_url != NULL ? rtsp_url : "none", codec, rtp_jitter, screen_width); - +#else + fprintf(stderr, "%s [-p mavlink_port]\n", argv[0]); + fprintf(stderr, "Default: mavlink_port=%d\n", osd_port); +#endif fprintf(stderr, "WFB-ng OSD version " WFB_OSD_VERSION "\n"); fprintf(stderr, "WFB-ng home page: \n"); exit(1); @@ -155,6 +166,7 @@ int main(int argc, char **argv) goto show_usage; } +#ifdef __GST_OPENGL__ printf("Use: mavlink_port=%d, rtp_port=%d, rtsp_url=%s, codec=%s, rtp_jitter=%d, osd_render=%d, screen_width=%d\n", osd_port, rtp_port, rtsp_url != NULL ? rtsp_url : "none", @@ -163,7 +175,6 @@ int main(int argc, char **argv) osd_init(0, 0, 1, 1); fd = open_udp_socket_for_rx(osd_port); -#ifdef __GST_OPENGL__ void* gst_thread_start(void *arg) { gst_main(rtp_port, codec, rtp_jitter, osd_render, screen_width, rtsp_url); @@ -191,7 +202,13 @@ int main(int argc, char **argv) exit(1); } } + #else + printf("Use mavlink_port=%d\n", osd_port); + + osd_init(0, 0, 1, 1); + fd = open_udp_socket_for_rx(osd_port); + if(fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK) < 0) { perror("Unable to set socket into nonblocked mode"); @@ -202,7 +219,11 @@ int main(int argc, char **argv) fds[0].fd = fd; fds[0].events = POLLIN; - while(1) + signal(SIGTERM, sigterm_handler); + signal(SIGINT, sigterm_handler); + + fprintf(stderr, "Starting event loop\n"); + while(!finished) { cur_ts = GetSystimeMS(); uint64_t sleep_ts = render_ts > cur_ts ? render_ts - cur_ts : 0; @@ -235,10 +256,11 @@ int main(int argc, char **argv) cur_ts = GetSystimeMS(); if (render_ts <= cur_ts) { - render_ts = cur_ts + 1000 / 10; // 10 Hz max + render_ts = cur_ts + 1000 / 30; // 30Hz osd refresh rate render(); } } + fprintf(stderr, "Event loop finished\n"); #endif return 0; } diff --git a/rpi_setup.sh b/rpi_setup.sh index ad1f211..4819a38 100755 --- a/rpi_setup.sh +++ b/rpi_setup.sh @@ -5,7 +5,7 @@ set -x apt-get update apt-get install build-essential libraspberrypi0 libraspberrypi-bin gstreamer1.0-tools gstreamer1.0-plugins-good -make mode=rpi codec=h264 +make mode=rpi3 cp -a osd fpv_video/{fpv_video,fpv_video.sh} /usr/bin