Change the type of cursor_plane from a weston_plane (base tracking
structure) to a drm_plane (wrapper containing additional DRM-specific
details), and make it a dynamically-allocated pointer.

Using the standard drm_plane allows us to reuse code which already deals
with drm_planes, e.g. a common cleanup function.

This patch introduces a 'special plane' helper, creating a drm_plane
either from a real KMS plane when using universal planes, or a fake plane
otherwise. Without universal planes, the cursor and primary planes are
hidden from us; this helper allows us to pretend otherwise.

Signed-off-by: Daniel Stone <dani...@collabora.com>
---
 libweston/compositor-drm.c | 423 ++++++++++++++++++++++++++++++++++-----------
 1 file changed, 322 insertions(+), 101 deletions(-)

diff --git a/libweston/compositor-drm.c b/libweston/compositor-drm.c
index 0827e953..33555d00 100644
--- a/libweston/compositor-drm.c
+++ b/libweston/compositor-drm.c
@@ -350,7 +350,7 @@ struct drm_output {
        int disable_pending;
 
        struct drm_fb *gbm_cursor_fb[2];
-       struct weston_plane cursor_plane;
+       struct drm_plane *cursor_plane;
        struct weston_view *cursor_view;
        int current_cursor;
 
@@ -644,7 +644,7 @@ drm_property_info_free(struct drm_property_info *info, int 
num_props)
 }
 
 static void
-drm_output_set_cursor(struct drm_output *output);
+drm_output_set_cursor(struct drm_output_state *output_state);
 
 static void
 drm_output_update_msc(struct drm_output *output, unsigned int seq);
@@ -1066,12 +1066,11 @@ drm_plane_state_put_back(struct drm_plane_state *state)
 }
 
 /**
- * Return a plane state from a drm_output_state, either existing or
- * freshly allocated.
+ * Return a plane state from a drm_output_state.
  */
 static struct drm_plane_state *
-drm_output_state_get_plane(struct drm_output_state *state_output,
-                          struct drm_plane *plane)
+drm_output_state_get_existing_plane(struct drm_output_state *state_output,
+                                   struct drm_plane *plane)
 {
        struct drm_plane_state *ps;
 
@@ -1080,6 +1079,23 @@ drm_output_state_get_plane(struct drm_output_state 
*state_output,
                        return ps;
        }
 
+       return NULL;
+}
+
+/**
+ * Return a plane state from a drm_output_state, either existing or
+ * freshly allocated.
+ */
+static struct drm_plane_state *
+drm_output_state_get_plane(struct drm_output_state *state_output,
+                          struct drm_plane *plane)
+{
+       struct drm_plane_state *ps;
+
+       ps = drm_output_state_get_existing_plane(state_output, plane);
+       if (ps)
+               return ps;
+
        return drm_plane_state_alloc(state_output, plane);
 }
 
@@ -1604,8 +1620,8 @@ drm_output_repaint(struct weston_output *output_base,
         */
        if (output->base.disable_planes) {
                output->cursor_view = NULL;
-               output->cursor_plane.x = INT32_MIN;
-               output->cursor_plane.y = INT32_MIN;
+               output->cursor_plane->base.x = INT32_MIN;
+               output->cursor_plane->base.y = INT32_MIN;
        }
 
        drm_output_render(state, damage);
@@ -1646,7 +1662,7 @@ drm_output_repaint(struct weston_output *output_base,
                wl_event_source_timer_update(output->pageflip_timer,
                                             backend->pageflip_timeout);
 
-       drm_output_set_cursor(output);
+       drm_output_set_cursor(state);
 
        /*
         * Now, update all the sprite surfaces
@@ -2169,20 +2185,66 @@ err:
        return NULL;
 }
 
+/**
+ * Update the image for the current cursor surface
+ *
+ * @param b DRM backend structure
+ * @param bo GBM buffer object to write into
+ * @param ev View to use for cursor image
+ */
+static void
+cursor_bo_update(struct drm_backend *b, struct gbm_bo *bo,
+                struct weston_view *ev)
+{
+       struct weston_buffer *buffer = ev->surface->buffer_ref.buffer;
+       uint32_t buf[b->cursor_width * b->cursor_height];
+       int32_t stride;
+       uint8_t *s;
+       int i;
+
+       assert(buffer && buffer->shm_buffer);
+       assert(buffer->shm_buffer == wl_shm_buffer_get(buffer->resource));
+       assert(ev->surface->width <= b->cursor_width);
+       assert(ev->surface->height <= b->cursor_height);
+
+       memset(buf, 0, sizeof buf);
+       stride = wl_shm_buffer_get_stride(buffer->shm_buffer);
+       s = wl_shm_buffer_get_data(buffer->shm_buffer);
+
+       wl_shm_buffer_begin_access(buffer->shm_buffer);
+       for (i = 0; i < ev->surface->height; i++)
+               memcpy(buf + i * b->cursor_width,
+                      s + i * stride,
+                      ev->surface->width * 4);
+       wl_shm_buffer_end_access(buffer->shm_buffer);
+
+       if (gbm_bo_write(bo, buf, sizeof buf) < 0)
+               weston_log("failed update cursor: %m\n");
+}
+
 static struct weston_plane *
 drm_output_prepare_cursor_view(struct drm_output_state *output_state,
                               struct weston_view *ev)
 {
        struct drm_output *output = output_state->output;
        struct drm_backend *b = to_drm_backend(output->base.compositor);
+       struct drm_plane *plane = output->cursor_plane;
+       struct drm_plane_state *plane_state;
        struct weston_buffer_viewport *viewport = &ev->surface->buffer_viewport;
        struct wl_shm_buffer *shmbuf;
+       bool needs_update = false;
        float x, y;
 
+       if (!plane)
+               return NULL;
+
        if (b->cursors_are_broken)
                return NULL;
 
-       if (output->cursor_view)
+       if (!plane->state_cur->complete)
+               return NULL;
+
+       if (plane->state_cur->output && plane->state_cur->output != output)
                return NULL;
 
        /* Don't import buffers which span multiple outputs. */
@@ -2215,89 +2277,101 @@ drm_output_prepare_cursor_view(struct drm_output_state 
*output_state,
            ev->surface->height > b->cursor_height)
                return NULL;
 
-       output->cursor_view = ev;
-       weston_view_to_global_float(ev, 0, 0, &x, &y);
-       output->cursor_plane.x = x;
-       output->cursor_plane.y = y;
+       plane_state =
+               drm_output_state_get_plane(output_state, output->cursor_plane);
 
-       return &output->cursor_plane;
-}
-
-/**
- * Update the image for the current cursor surface
- *
- * @param b DRM backend structure
- * @param bo GBM buffer object to write into
- * @param ev View to use for cursor image
- */
-static void
-cursor_bo_update(struct drm_backend *b, struct gbm_bo *bo,
-                struct weston_view *ev)
-{
-       struct weston_buffer *buffer = ev->surface->buffer_ref.buffer;
-       uint32_t buf[b->cursor_width * b->cursor_height];
-       int32_t stride;
-       uint8_t *s;
-       int i;
-
-       assert(buffer && buffer->shm_buffer);
-       assert(buffer->shm_buffer == wl_shm_buffer_get(buffer->resource));
-       assert(ev->surface->width <= b->cursor_width);
-       assert(ev->surface->height <= b->cursor_height);
-
-       memset(buf, 0, sizeof buf);
-       stride = wl_shm_buffer_get_stride(buffer->shm_buffer);
-       s = wl_shm_buffer_get_data(buffer->shm_buffer);
+       if (plane_state && plane_state->fb)
+               return NULL;
 
-       wl_shm_buffer_begin_access(buffer->shm_buffer);
-       for (i = 0; i < ev->surface->height; i++)
-               memcpy(buf + i * b->cursor_width,
-                      s + i * stride,
-                      ev->surface->width * 4);
-       wl_shm_buffer_end_access(buffer->shm_buffer);
+       /* Since we're setting plane state up front, we need to work out
+        * whether or not we need to upload a new cursor. We can't use the
+        * plane damage, since the planes haven't actually been calculated
+        * yet: instead try to figure it out directly. KMS cursor planes are
+        * pretty unique here, in that they lie partway between a Weston plane
+        * (direct scanout) and a renderer. */
+       if (ev != output->cursor_view ||
+           pixman_region32_not_empty(&ev->surface->damage)) {
+               output->current_cursor++;
+               output->current_cursor =
+                       output->current_cursor %
+                               ARRAY_LENGTH(output->gbm_cursor_fb);
+               needs_update = true;
+       }
 
-       if (gbm_bo_write(bo, buf, sizeof buf) < 0)
-               weston_log("failed update cursor: %m\n");
+       output->cursor_view = ev;
+       weston_view_to_global_float(ev, 0, 0, &x, &y);
+       plane->base.x = x;
+       plane->base.y = y;
+
+       plane_state->fb =
+               drm_fb_ref(output->gbm_cursor_fb[output->current_cursor]);
+       plane_state->output = output;
+       plane_state->src_x = 0;
+       plane_state->src_y = 0;
+       plane_state->src_w = b->cursor_width << 16;
+       plane_state->src_h = b->cursor_height << 16;
+       plane_state->dest_x = (x - output->base.x) * output->base.current_scale;
+       plane_state->dest_y = (y - output->base.y) * output->base.current_scale;
+       plane_state->dest_w = b->cursor_width;
+       plane_state->dest_h = b->cursor_height;
+
+       if (needs_update)
+               cursor_bo_update(b, plane_state->fb->bo, ev);
+
+       return &plane->base;
 }
 
 static void
-drm_output_set_cursor(struct drm_output *output)
+drm_output_set_cursor(struct drm_output_state *output_state)
 {
-       struct weston_view *ev = output->cursor_view;
+       struct drm_output *output = output_state->output;
        struct drm_backend *b = to_drm_backend(output->base.compositor);
+       struct drm_plane *plane = output->cursor_plane;
+       struct drm_plane_state *state;
        EGLint handle;
        struct gbm_bo *bo;
-       float x, y;
 
-       if (ev == NULL) {
+       if (!plane)
+               return;
+
+       state = drm_output_state_get_existing_plane(output_state, plane);
+       if (!state)
+               return;
+
+       if (!state->fb) {
+               pixman_region32_fini(&plane->base.damage);
+               pixman_region32_init(&plane->base.damage);
                drmModeSetCursor(b->drm.fd, output->crtc_id, 0, 0, 0);
                return;
        }
 
-       if (pixman_region32_not_empty(&output->cursor_plane.damage)) {
-               pixman_region32_fini(&output->cursor_plane.damage);
-               pixman_region32_init(&output->cursor_plane.damage);
-               output->current_cursor ^= 1;
-               bo = output->gbm_cursor_fb[output->current_cursor]->bo;
+       assert(state->fb == output->gbm_cursor_fb[output->current_cursor]);
+       assert(!plane->state_cur->output || plane->state_cur->output == output);
 
-               cursor_bo_update(b, bo, ev);
+       if (plane->state_cur->fb != state->fb) {
+               bo = state->fb->bo;
                handle = gbm_bo_get_handle(bo).s32;
                if (drmModeSetCursor(b->drm.fd, output->crtc_id, handle,
-                               b->cursor_width, b->cursor_height)) {
+                                    b->cursor_width, b->cursor_height)) {
                        weston_log("failed to set cursor: %m\n");
-                       b->cursors_are_broken = 1;
+                       goto err;
                }
        }
 
-       x = (output->cursor_plane.x - output->base.x) *
-               output->base.current_scale;
-       y = (output->cursor_plane.y - output->base.y) *
-               output->base.current_scale;
+       pixman_region32_fini(&plane->base.damage);
+       pixman_region32_init(&plane->base.damage);
 
-       if (drmModeMoveCursor(b->drm.fd, output->crtc_id, x, y)) {
+       if (drmModeMoveCursor(b->drm.fd, output->crtc_id,
+                             state->dest_x, state->dest_y)) {
                weston_log("failed to move cursor: %m\n");
-               b->cursors_are_broken = 1;
+               goto err;
        }
+
+       return;
+
+err:
+       b->cursors_are_broken = 1;
+       drmModeSetCursor(b->drm.fd, output->crtc_id, 0, 0, 0);
 }
 
 static void
@@ -2307,6 +2381,7 @@ drm_assign_planes(struct weston_output *output_base, void 
*repaint_data)
        struct drm_pending_state *pending_state = repaint_data;
        struct drm_output *output = to_drm_output(output_base);
        struct drm_output_state *state;
+       struct drm_plane_state *plane_state;
        struct weston_view *ev, *next;
        pixman_region32_t overlap, surface_overlap;
        struct weston_plane *primary, *next_plane;
@@ -2332,10 +2407,6 @@ drm_assign_planes(struct weston_output *output_base, 
void *repaint_data)
        pixman_region32_init(&overlap);
        primary = &output_base->compositor->primary_plane;
 
-       output->cursor_view = NULL;
-       output->cursor_plane.x = INT32_MIN;
-       output->cursor_plane.y = INT32_MIN;
-
        wl_list_for_each_safe(ev, next, &output_base->compositor->view_list, 
link) {
                struct weston_surface *es = ev->surface;
 
@@ -2379,7 +2450,8 @@ drm_assign_planes(struct weston_output *output_base, void 
*repaint_data)
                                              &ev->transform.boundingbox);
 
                if (next_plane == primary ||
-                   next_plane == &output->cursor_plane) {
+                   (output->cursor_plane &&
+                    next_plane == &output->cursor_plane->base)) {
                        /* cursor plane involves a copy */
                        ev->psf_flags = 0;
                } else {
@@ -2392,6 +2464,19 @@ drm_assign_planes(struct weston_output *output_base, 
void *repaint_data)
                pixman_region32_fini(&surface_overlap);
        }
        pixman_region32_fini(&overlap);
+
+       /* We rely on output->cursor_view being both an accurate reflection of
+        * the cursor plane's state, but also being maintained across repaints
+        * to avoid unnecessary damage uploads, per the comment in
+        * drm_output_prepare_cursor_view. In the event that we go from having
+        * a cursor view to not having a cursor view, we need to clear it. */
+       if (output->cursor_view) {
+               plane_state =
+                       drm_output_state_get_existing_plane(state,
+                                                           
output->cursor_plane);
+               if (!plane_state || !plane_state->fb)
+                       output->cursor_view = NULL;
+       }
 }
 
 /**
@@ -2659,19 +2744,30 @@ init_pixman(struct drm_backend *b)
  * Creates one drm_plane structure for a hardware plane, and initialises its
  * properties and formats.
  *
+ * In the absence of universal plane support, where KMS does not explicitly
+ * expose the primary and cursor planes to userspace, this may also create
+ * an 'internal' plane for internal management.
+ *
  * This function does not add the plane to the list of usable planes in Weston
  * itself; the caller is responsible for this.
  *
  * Call drm_plane_destroy to clean up the plane.
  *
+ * @sa drm_output_find_special_plane
  * @param b DRM compositor backend
- * @param kplane DRM plane to create
+ * @param kplane DRM plane to create, or NULL if creating internal plane
+ * @param output Output to create internal plane for, or NULL
+ * @param type Type to use when creating internal plane, or invalid
+ * @param format Format to use for internal planes, or 0
  */
 static struct drm_plane *
-drm_plane_create(struct drm_backend *b, const drmModePlane *kplane)
+drm_plane_create(struct drm_backend *b, const drmModePlane *kplane,
+                struct drm_output *output, enum wdrm_plane_type type,
+                uint32_t format)
 {
        struct drm_plane *plane;
        drmModeObjectProperties *props;
+       int num_formats = (kplane) ? kplane->count_formats : 1;
 
        static struct drm_property_enum_info plane_type_enums[] = {
                [WDRM_PLANE_TYPE_PRIMARY] = {
@@ -2692,36 +2788,61 @@ drm_plane_create(struct drm_backend *b, const 
drmModePlane *kplane)
                },
        };
 
-       plane = zalloc(sizeof(*plane) + ((sizeof(uint32_t)) *
-                                         kplane->count_formats));
+       plane = zalloc(sizeof(*plane) +
+                      (sizeof(uint32_t) * num_formats));
        if (!plane) {
                weston_log("%s: out of memory\n", __func__);
                return NULL;
        }
 
        plane->backend = b;
-       plane->possible_crtcs = kplane->possible_crtcs;
-       plane->plane_id = kplane->plane_id;
-       plane->count_formats = kplane->count_formats;
        plane->state_cur = drm_plane_state_alloc(NULL, plane);
        plane->state_cur->complete = true;
-       memcpy(plane->formats, kplane->formats,
-              kplane->count_formats * sizeof(kplane->formats[0]));
 
-       props = drmModeObjectGetProperties(b->drm.fd, kplane->plane_id,
-                                          DRM_MODE_OBJECT_PLANE);
-       if (!props) {
-               weston_log("couldn't get plane properties\n");
+       if (kplane) {
+               plane->possible_crtcs = kplane->possible_crtcs;
+               plane->plane_id = kplane->plane_id;
+               plane->count_formats = kplane->count_formats;
+               memcpy(plane->formats, kplane->formats,
+                      kplane->count_formats * sizeof(kplane->formats[0]));
+
+               props = drmModeObjectGetProperties(b->drm.fd, kplane->plane_id,
+                                                  DRM_MODE_OBJECT_PLANE);
+               if (!props) {
+                       weston_log("couldn't get plane properties\n");
+                       free(plane);
+                       return NULL;
+               }
+               drm_property_info_populate(b, plane_props, plane->props,
+                                          WDRM_PLANE__COUNT, props);
+               plane->type =
+                       drm_property_get_value(&plane->props[WDRM_PLANE_TYPE],
+                                              props,
+                                              WDRM_PLANE_TYPE__COUNT);
+               drmModeFreeObjectProperties(props);
+       }
+       else {
+               plane->possible_crtcs = (1 << output->pipe);
+               plane->plane_id = 0;
+               plane->count_formats = 1;
+               plane->formats[0] = format;
+               plane->type = type;
+       }
+
+       if (plane->type == WDRM_PLANE_TYPE__COUNT) {
                free(plane);
                return NULL;
        }
-       drm_property_info_populate(b, plane_props, plane->props,
-                                  WDRM_PLANE__COUNT, props);
-       plane->type =
-               drm_property_get_value(&plane->props[WDRM_PLANE_TYPE],
-                                      props,
-                                      WDRM_PLANE_TYPE_OVERLAY);
-       drmModeFreeObjectProperties(props);
+
+       /* With universal planes, everything is a DRM plane; without
+        * universal planes, the only DRM planes are overlay planes. */
+       if (b->universal_planes) {
+               assert(kplane);
+       } else {
+               assert((kplane && plane->type == WDRM_PLANE_TYPE_OVERLAY) ||
+                      (!kplane && plane->type != WDRM_PLANE_TYPE_OVERLAY &&
+                       output));
+       }
 
        weston_plane_init(&plane->base, b->compositor, 0, 0);
        wl_list_insert(&b->plane_list, &plane->link);
@@ -2729,6 +2850,88 @@ drm_plane_create(struct drm_backend *b, const 
drmModePlane *kplane)
        return plane;
 }
 
+/**
+ * Find, or create, a special-purpose plane
+ *
+ * Primary and cursor planes are a special case, in that before universal
+ * planes, they are driven by non-plane API calls. Without universal plane
+ * support, the only way to configure a primary plane is via drmModeSetCrtc,
+ * and the only way to configure a cursor plane is drmModeSetCursor2.
+ *
+ * Although they may actually be regular planes in the hardware, without
+ * universal plane support, these planes are not actually exposed to
+ * userspace in the regular plane list.
+ *
+ * However, for ease of internal tracking, we want to manage all planes
+ * through the same drm_plane structures. Therefore, when we are running
+ * without universal plane support, we create fake drm_plane structures
+ * to track these planes.
+ *
+ * @param b DRM backend
+ * @param output Output to use for plane
+ * @param type Type of plane
+ */
+static struct drm_plane *
+drm_output_find_special_plane(struct drm_backend *b, struct drm_output *output,
+                             enum wdrm_plane_type type)
+{
+       struct drm_plane *plane;
+
+       if (!b->universal_planes) {
+               uint32_t format;
+
+               switch (type) {
+               case WDRM_PLANE_TYPE_CURSOR:
+                       format = GBM_FORMAT_ARGB8888;
+                       break;
+               case WDRM_PLANE_TYPE_PRIMARY:
+                       format = output->gbm_format;
+                       break;
+               default:
+                       assert(!"invalid type in 
drm_output_find_special_plane");
+                       break;
+               }
+
+               return drm_plane_create(b, NULL, output, type, format);
+       }
+
+       wl_list_for_each(plane, &b->plane_list, link) {
+               struct drm_output *tmp;
+               bool found_elsewhere = false;
+
+               if (plane->type != type)
+                       continue;
+               if (!drm_plane_crtc_supported(output, plane))
+                       continue;
+
+               /* On some platforms, primary/cursor planes can roam
+                * between different CRTCs, so make sure we don't claim the
+                * same plane for two outputs. */
+               wl_list_for_each(tmp, &b->compositor->pending_output_list,
+                                base.link) {
+                       if (tmp->cursor_plane == plane) {
+                               found_elsewhere = true;
+                               break;
+                       }
+               }
+               wl_list_for_each(tmp, &b->compositor->output_list,
+                                base.link) {
+                       if (tmp->cursor_plane == plane) {
+                               found_elsewhere = true;
+                               break;
+                       }
+               }
+
+               if (found_elsewhere)
+                       continue;
+
+               plane->possible_crtcs = (1 << output->pipe);
+               return plane;
+       }
+
+       return NULL;
+}
+
 /**
  * Destroy one DRM plane
  *
@@ -2778,7 +2981,8 @@ create_sprites(struct drm_backend *b)
                if (!kplane)
                        continue;
 
-               drm_plane = drm_plane_create(b, kplane);
+               drm_plane = drm_plane_create(b, kplane, NULL,
+                                            WDRM_PLANE_TYPE__COUNT, 0);
                drmModeFreePlane(kplane);
                if (!drm_plane)
                        continue;
@@ -3044,6 +3248,10 @@ drm_output_init_cursor_egl(struct drm_output *output, 
struct drm_backend *b)
 {
        unsigned int i;
 
+       /* No point creating cursors if we don't have a plane for them. */
+       if (!output->cursor_plane)
+               return 0;
+
        for (i = 0; i < ARRAY_LENGTH(output->gbm_cursor_fb); i++) {
                struct gbm_bo *bo;
 
@@ -3631,11 +3839,15 @@ drm_output_enable(struct weston_output *base)
            output->connector->connector_type == DRM_MODE_CONNECTOR_eDP)
                output->base.connection_internal = true;
 
-       weston_plane_init(&output->cursor_plane, b->compositor,
-                         INT32_MIN, INT32_MIN);
        weston_plane_init(&output->scanout_plane, b->compositor, 0, 0);
 
-       weston_compositor_stack_plane(b->compositor, &output->cursor_plane, 
NULL);
+       if (output->cursor_plane)
+               weston_compositor_stack_plane(b->compositor,
+                                             &output->cursor_plane->base,
+                                             NULL);
+       else
+               b->cursors_are_broken = 1;
+
        weston_compositor_stack_plane(b->compositor, &output->scanout_plane,
                                      &b->compositor->primary_plane);
 
@@ -3678,10 +3890,11 @@ drm_output_deinit(struct weston_output *base)
                drm_output_fini_egl(output);
 
        weston_plane_release(&output->scanout_plane);
-       weston_plane_release(&output->cursor_plane);
 
-       /* Turn off hardware cursor */
-       drmModeSetCursor(b->drm.fd, output->crtc_id, 0, 0, 0);
+       if (output->cursor_plane) {
+               /* Turn off hardware cursor */
+               drmModeSetCursor(b->drm.fd, output->crtc_id, 0, 0, 0);
+       }
 }
 
 static void
@@ -3845,6 +4058,12 @@ create_output_for_connector(struct drm_backend *b,
                }
        }
 
+       /* Failing to find a cursor plane is not fatal, as we'll fall back
+        * to software cursor. */
+       output->cursor_plane =
+               drm_output_find_special_plane(b, output,
+                                             WDRM_PLANE_TYPE_CURSOR);
+
        weston_compositor_add_pending_output(&output->base, b->compositor);
 
        return 0;
@@ -4083,7 +4302,9 @@ session_notify(struct wl_listener *listener, void *data)
 
                wl_list_for_each(output, &compositor->output_list, base.link) {
                        output->base.repaint_needed = false;
-                       drmModeSetCursor(b->drm.fd, output->crtc_id, 0, 0, 0);
+                       if (output->cursor_plane)
+                               drmModeSetCursor(b->drm.fd, output->crtc_id,
+                                                0, 0, 0);
                }
 
                output = container_of(compositor->output_list.next,
-- 
2.14.1

_______________________________________________
wayland-devel mailing list
wayland-devel@lists.freedesktop.org
https://lists.freedesktop.org/mailman/listinfo/wayland-devel

Reply via email to