/* * OMAP3 ISP library - OMAP3 ISP * * Copyright (C) 2010-2011 Ideas on board SPRL * * Contact: Laurent Pinchart * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or (at * your option) any later version. * * This library 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 Lesser General Public License * along with this library; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include #include #include #include #include #include #include "controls.h" #include "list.h" #include "omap3isp-priv.h" #include "omap3isp.h" #include "subdev.h" #define ENTITY_CCDC "OMAP3 ISP CCDC" #define ENTITY_CCDC_OUTPUT "OMAP3 ISP CCDC output" #define ENTITY_PREVIEW_INPUT "OMAP3 ISP preview input" #define ENTITY_PREVIEW "OMAP3 ISP preview" #define ENTITY_RESIZER "OMAP3 ISP resizer" #define ENTITY_RESIZER_INPUT "OMAP3 ISP resizer input" #define ENTITY_RESIZER_OUTPUT "OMAP3 ISP resizer output" /* ----------------------------------------------------------------------------- * Helper functions */ static int setup_link(struct omap3_isp_device *isp, struct media_entity *source, struct media_entity *sink, __u32 flags) { unsigned int i; int ret; for (i = 0; i < source->num_links; ++i) { if (source->links[i].source->entity == source && source->links[i].sink->entity == sink) break; } if (i == source->num_links) return -ENOENT; ret = media_setup_link(isp->mdev, source->links[i].source, source->links[i].sink, flags); if (ret < 0) printf("error: unable to %s %s -> %s link.\n", flags & MEDIA_LNK_FL_ENABLED ? "enable" : "disable", source->info.name, sink->info.name); return ret; } /* * struct isp_format_info - ISP media bus format information * @code: V4L2 media bus format code * @flavor: V4L2 media bus format code for the same pixel layout but * shifted to be 8 bits per pixel. =0 if format is not shiftable. * @pixelformat: V4L2 pixel format FCC identifier */ struct isp_format_info { enum v4l2_mbus_pixelcode code; enum v4l2_mbus_pixelcode flavor; __u32 pixelformat; }; static const struct isp_format_info formats[] = { { V4L2_MBUS_FMT_Y8_1X8, V4L2_MBUS_FMT_Y8_1X8, V4L2_PIX_FMT_GREY, }, { V4L2_MBUS_FMT_Y10_1X10, V4L2_MBUS_FMT_Y8_1X8, V4L2_PIX_FMT_Y10, }, { V4L2_MBUS_FMT_Y12_1X12, V4L2_MBUS_FMT_Y8_1X8, V4L2_PIX_FMT_Y12, }, { V4L2_MBUS_FMT_SBGGR8_1X8, V4L2_MBUS_FMT_SBGGR8_1X8, V4L2_PIX_FMT_SBGGR8, }, { V4L2_MBUS_FMT_SGBRG8_1X8, V4L2_MBUS_FMT_SGBRG8_1X8, V4L2_PIX_FMT_SGBRG8, }, { V4L2_MBUS_FMT_SGRBG8_1X8, V4L2_MBUS_FMT_SGRBG8_1X8, V4L2_PIX_FMT_SGRBG8, }, { V4L2_MBUS_FMT_SRGGB8_1X8, V4L2_MBUS_FMT_SRGGB8_1X8, V4L2_PIX_FMT_SRGGB8, }, { V4L2_MBUS_FMT_SBGGR10_1X10, V4L2_MBUS_FMT_SBGGR8_1X8, V4L2_PIX_FMT_SBGGR10, }, { V4L2_MBUS_FMT_SGBRG10_1X10, V4L2_MBUS_FMT_SGBRG8_1X8, V4L2_PIX_FMT_SGBRG10, }, { V4L2_MBUS_FMT_SGRBG10_1X10, V4L2_MBUS_FMT_SGRBG8_1X8, V4L2_PIX_FMT_SGRBG10, }, { V4L2_MBUS_FMT_SRGGB10_1X10, V4L2_MBUS_FMT_SRGGB8_1X8, V4L2_PIX_FMT_SRGGB10, }, { V4L2_MBUS_FMT_SBGGR12_1X12, V4L2_MBUS_FMT_SBGGR8_1X8, V4L2_PIX_FMT_SBGGR12, }, { V4L2_MBUS_FMT_SGBRG12_1X12, V4L2_MBUS_FMT_SGBRG8_1X8, V4L2_PIX_FMT_SGBRG12, }, { V4L2_MBUS_FMT_SGRBG12_1X12, V4L2_MBUS_FMT_SGRBG8_1X8, V4L2_PIX_FMT_SGRBG12, }, { V4L2_MBUS_FMT_SRGGB12_1X12, V4L2_MBUS_FMT_SRGGB8_1X8, V4L2_PIX_FMT_SRGGB12, }, { V4L2_MBUS_FMT_UYVY8_1X16, 0, V4L2_PIX_FMT_UYVY, }, { V4L2_MBUS_FMT_YUYV8_1X16, 0, V4L2_PIX_FMT_YUYV, }, }; const struct isp_format_info * omap3_isp_format_info(enum v4l2_mbus_pixelcode code) { unsigned int i; for (i = 0; i < ARRAY_SIZE(formats); ++i) { if (formats[i].code == code) return &formats[i]; } return NULL; } /* * Decide whether desired output pixel code can be obtained with * the lane shifter by shifting the input pixel code. * @in: input pixelcode to shifter * @out: output pixelcode from shifter * * return true if the combination is possible * return false otherwise */ static bool format_is_shiftable(enum v4l2_mbus_pixelcode in, enum v4l2_mbus_pixelcode out) { const struct isp_format_info *in_info, *out_info; if (in == out) return true; in_info = omap3_isp_format_info(in); out_info = omap3_isp_format_info(out); if ((in_info->flavor == 0) || (out_info->flavor == 0)) return false; return in_info->flavor == out_info->flavor; } static __u32 mbus_to_pix(enum v4l2_mbus_pixelcode code) { unsigned int i; for (i = 0; i < ARRAY_SIZE(formats); ++i) { if (formats[i].code == code) return formats[i].pixelformat; } return 0; } static enum v4l2_mbus_pixelcode pix_to_mbus(__u32 pixelformat) { unsigned int i; for (i = 0; i < ARRAY_SIZE(formats); ++i) { if (formats[i].pixelformat == pixelformat) return formats[i].code; } return 0; } static const char *entity_name(struct omap3_isp_entity *entity) { if (entity == NULL) return "NULL"; if (entity->type == OMAP3_ISP_ENTITY_POOL) return "POOL"; return entity->entity->info.name; } /* ----------------------------------------------------------------------------- * Video devices */ static int omap3_isp_video_start(struct omap3_isp_video *video) { int ret; ret = v4l2_stream_on(video->video); if (ret < 0) return ret; video->running = true; return 0; } static int omap3_isp_video_stop(struct omap3_isp_video *video) { video->queued = 0; video->running = false; return v4l2_stream_off(video->video); } static int omap3_isp_video_queue_buffer(struct omap3_isp_video *video, struct v4l2_video_buffer *buffer) { video->queued++; return v4l2_queue_buffer(video->video, buffer); } static int omap3_isp_video_dequeue_buffer(struct omap3_isp_video *video, struct v4l2_video_buffer *buffer) { int ret; ret = v4l2_dequeue_buffer(video->video, buffer); if (ret < 0) return ret; video->queued--; return 0; } /* ----------------------------------------------------------------------------- * Pools */ static void omap3_isp_pool_event_input(void *priv); static void omap3_isp_pool_event_output(void *priv); static void omap3_isp_pool_event(struct omap3_isp_pool *pool, struct omap3_isp_video *from, struct omap3_isp_video *to, bool input) { struct omap3_isp_device *isp = pool->isp; struct v4l2_video_buffer buffer; int ret; /* Dequeue the buffer */ ret = omap3_isp_video_dequeue_buffer(from, &buffer); if (ret < 0) { printf("error: pool: unable to dequeue buffer: %s (%d)\n", strerror(-ret), ret); return; } if (from->queued == 0) isp->ops->unwatch_fd(from->video->fd); /* Queue it on the other side */ if (to->queued == 0) { if (input) isp->ops->watch_fd(to->video->fd, OMAP3_ISP_EVENT_WRITE, omap3_isp_pool_event_output, pool); else isp->ops->watch_fd(to->video->fd, OMAP3_ISP_EVENT_READ, omap3_isp_pool_event_input, pool); } ret = omap3_isp_video_queue_buffer(to, &buffer); if (ret < 0) printf("error: pool: unable to requeue buffer: %s (%d)\n", strerror(-ret), ret); } static void omap3_isp_pool_event_input(void *priv) { struct omap3_isp_pool *pool = priv; omap3_isp_pool_event(pool, pool->input, pool->output, true); } static void omap3_isp_pool_event_output(void *priv) { struct omap3_isp_pool *pool = priv; omap3_isp_pool_event(pool, pool->output, pool->input, false); } static int omap3_isp_pool_start(struct omap3_isp_pool *pool) { struct v4l2_video_buffer buffer; unsigned int i; int ret; /* Queue all buffers for video capture. */ for (i = 0; i < pool->pool->nbufs; ++i) { buffer.index = i; ret = omap3_isp_video_queue_buffer(pool->input, &buffer); if (ret < 0) { printf("error: pool: unable to queue buffer %u (%d)\n", i, ret); return ret; } } /* Watch the pool input file descriptor. */ pool->isp->ops->watch_fd(pool->input->video->fd, OMAP3_ISP_EVENT_READ, omap3_isp_pool_event_input, pool); ret = omap3_isp_video_start(pool->input); if (ret < 0) { printf("error: pool: streamon failed on input (%d)\n", ret); return ret; } ret = omap3_isp_video_start(pool->output); if (ret < 0) { printf("error: pool: streamon failed on output (%d)\n", ret); return ret; } return 0; } static void omap3_isp_pool_stop(struct omap3_isp_pool *pool) { int ret; pool->isp->ops->unwatch_fd(pool->input->video->fd); pool->isp->ops->unwatch_fd(pool->output->video->fd); ret = omap3_isp_video_stop(pool->input); if (ret < 0) printf("error: pool: streamoff failed for input (%d)\n", ret); ret = omap3_isp_video_stop(pool->output); if (ret < 0) printf("error: pool: streamoff failed for output (%d)\n", ret); } static int omap3_isp_pool_alloc_buffers(struct omap3_isp_pool *pool) { int ret; ret = v4l2_alloc_buffers(pool->output->video, pool->pool, V4L2_MEMORY_MMAP); if (ret < 0) { printf("error: pool: unable to allocate output buffers.\n"); return ret; } ret = v4l2_alloc_buffers(pool->input->video, pool->pool, V4L2_MEMORY_USERPTR); if (ret < 0) { printf("error: pool: unable to allocate input buffers.\n"); return ret; } return 0; } static void omap3_isp_pool_free_buffers(struct omap3_isp_pool *pool) { int ret; ret = v4l2_free_buffers(pool->input->video); if (ret < 0) printf("error: pool: unable to free input buffers.\n"); ret = v4l2_free_buffers(pool->output->video); if (ret < 0) printf("error: pool: unable to free output buffers.\n"); } /* ----------------------------------------------------------------------------- * Pipeline management */ static void omap3_isp_pipeline_init(struct omap3_isp_pipeline *pipe) { list_init(&pipe->entities); list_init(&pipe->pools); pipe->state = OMAP3_ISP_PIPELINE_STOPPED; } static void omap3_isp_pipeline_destroy(struct omap3_isp_pipeline *pipe) { struct omap3_isp_entity *entity; struct omap3_isp_entity *next; struct omap3_isp_video *video; struct omap3_isp_pool *pool; list_for_each_entry_safe(entity, next, &pipe->entities, list) { list_remove(&entity->list); switch (entity->type) { case OMAP3_ISP_ENTITY_POOL: pool = to_omap3_isp_pool(entity); v4l2_buffers_pool_delete(pool->pool); list_remove(&pool->list); free(pool); break; case OMAP3_ISP_ENTITY_VIDEO: video = to_omap3_isp_video(entity); v4l2_close(video->video); free(video); break; case OMAP3_ISP_ENTITY_ENTITY: v4l2_subdev_close(entity->entity); free(entity); break; } } } static struct omap3_isp_pool * omap3_isp_pipeline_create_pool(struct omap3_isp_device *isp, const char *name) { struct omap3_isp_pool *pool; unsigned int nbufs; char *endp; name += 5; if (*name != ':') return NULL; nbufs = strtoul(name + 1, &endp, 10); if (*endp != '\0') return NULL; pool = malloc(sizeof *pool); if (pool == NULL) return NULL; memset(pool, 0, sizeof *pool); pool->isp = isp; pool->entity.type = OMAP3_ISP_ENTITY_POOL; pool->pool = v4l2_buffers_pool_new(nbufs); if (pool->pool == NULL) { free(pool); return NULL; } return pool; } static struct omap3_isp_entity * omap3_isp_pipeline_create_entity(struct omap3_isp_device *isp, const char *name) { struct omap3_isp_entity *entity; struct omap3_isp_video *video; struct media_entity *ment; if (strncmp(name, ":SENSOR", 7) == 0) ment = isp->sensor.entity; else ment = media_get_entity_by_name(isp->mdev, name); if (ment == NULL) return NULL; if (media_entity_type(ment) == MEDIA_ENT_T_DEVNODE) { video = malloc(sizeof *video); if (video == NULL) return NULL; memset(video, 0, sizeof *video); video->isp = isp; video->video = v4l2_open(ment->devname); if (video->video == NULL) { printf("error: unable to open video device %s\n", ment->devname); return NULL; } entity = &video->entity; entity->type = OMAP3_ISP_ENTITY_VIDEO; } else { entity = malloc(sizeof *entity); if (entity == NULL) return NULL; memset(entity, 0, sizeof *entity); entity->type = OMAP3_ISP_ENTITY_ENTITY; } entity->entity = ment; return entity; } static int omap3_isp_pipeline_validate_link(struct omap3_isp_entity *source, struct omap3_isp_entity *sink) { struct omap3_isp_video *video; struct omap3_isp_pool *pool; unsigned int i; if (source == NULL) return 0; if (source->type != OMAP3_ISP_ENTITY_POOL && sink->type != OMAP3_ISP_ENTITY_POOL) { /* Make sure there's a link between the source and sink * entities. */ for (i = 0; i < source->entity->num_links; ++i) { if (source->entity->links[i].sink->entity == sink->entity) break; } if (i == source->entity->num_links) return -EPIPE; /* Store link information in source and sink */ source->source.link = &source->entity->links[i]; sink->sink.link = &source->entity->links[i]; return 0; } if (source->type == OMAP3_ISP_ENTITY_VIDEO && sink->type == OMAP3_ISP_ENTITY_POOL) { pool = to_omap3_isp_pool(sink); video = to_omap3_isp_video(source); pool->input = video; return 0; } if (source->type == OMAP3_ISP_ENTITY_POOL && sink->type == OMAP3_ISP_ENTITY_VIDEO) { pool = to_omap3_isp_pool(source); video = to_omap3_isp_video(sink); pool->output = video; return 0; } return -EPIPE; } static int omap3_isp_pipeline_build(struct omap3_isp_device *isp, struct omap3_isp_pipeline *pipe, ...) { struct omap3_isp_entity *source = NULL; struct omap3_isp_entity *sink; struct omap3_isp_pool *pool; bool first = true; va_list ap; char *name; int ret; va_start(ap, pipe); while ((name = va_arg(ap, char *)) != NULL) { if (strncmp(name, ":POOL", 5) == 0) { /* The previous entity must be a device node. */ if (source == NULL || media_entity_type(source->entity) != MEDIA_ENT_T_DEVNODE) return -EPIPE; pool = omap3_isp_pipeline_create_pool(isp, name); if (pool == NULL) { ret = -ENOMEM; goto done; } list_append(&pool->list, &pipe->pools); sink = &pool->entity; } else { sink = omap3_isp_pipeline_create_entity(isp, name); if (sink == NULL) { ret = -ENOMEM; goto done; } } if (first && sink->type == OMAP3_ISP_ENTITY_VIDEO) pipe->input = to_omap3_isp_video(sink); list_append(&sink->list, &pipe->entities); ret = omap3_isp_pipeline_validate_link(source, sink); if (ret < 0) { printf("No valid link found between %s and %s\n", entity_name(source), entity_name(sink)); goto done; } source = sink; first = false; } if (source == NULL || source->type != OMAP3_ISP_ENTITY_VIDEO) { ret = -EINVAL; goto done; } pipe->output = to_omap3_isp_video(source); source->last = true; va_end(ap); ret = 0; done: if (ret < 0) omap3_isp_pipeline_destroy(pipe); return ret; } static int omap3_isp_pipeline_try_format(struct omap3_isp_device *isp, struct omap3_isp_pipeline *pipe, const struct v4l2_mbus_framefmt *ifmt, struct v4l2_mbus_framefmt *ofmt, struct v4l2_rect *crop) { struct v4l2_mbus_framefmt format; struct v4l2_pix_format v4l2_fmt; struct omap3_isp_entity *source = NULL; struct omap3_isp_entity *sink; struct omap3_isp_video *video; struct media_entity_pad *pad; int ret; pipe->resizer.entity = NULL; /* Configure formats. Start from the input and propagate the format * through the pipeline. * * When scaling on the ISP, select the input format, otherwise scale as * much as possible on the sensor. */ if (pipe->scaler == OMAP3_ISP_SCALER_ISP) format = *ifmt; else format = *ofmt; list_for_each_entry(sink, &pipe->entities, list) { if (source == NULL) { /* Configure the first entity in the pipeline with the * initial format. */ pad = sink->source.link->source; if (sink->type == OMAP3_ISP_ENTITY_ENTITY) { ret = v4l2_subdev_set_format(sink->entity, &format, 0, V4L2_SUBDEV_FORMAT_TRY); if (ret < 0) { printf("error: get format on sensor output failed.\n"); return ret; } } else if (sink->type == OMAP3_ISP_ENTITY_VIDEO) { video = to_omap3_isp_video(sink); memset(&v4l2_fmt, 0, sizeof v4l2_fmt); v4l2_fmt.pixelformat = mbus_to_pix(format.code); v4l2_fmt.width = format.width; v4l2_fmt.height = format.height; ret = v4l2_set_format(video->video, &v4l2_fmt); if (ret < 0) { printf("error: set format failed on %s.\n", sink->entity->info.name); return ret; } format.code = pix_to_mbus(v4l2_fmt.pixelformat); format.width = v4l2_fmt.width; format.height = v4l2_fmt.height; video->format = format; } source = sink; continue; } if (source->type == OMAP3_ISP_ENTITY_ENTITY) { pad = source->source.link->source; /* Crop on the first subdev that supports cropping. */ source->source.crop.width = 0; source->source.crop.height = 0; if (crop) { ret = v4l2_subdev_set_crop(pad->entity, crop, pad->index, V4L2_SUBDEV_FORMAT_TRY); if (ret < 0 && ret != -ENOTTY && ret != -EINVAL) { printf("error: set crop on sensor output failed.\n"); return ret; } else if (ret >= 0) { format.width = crop->width; format.height = crop->height; source->source.crop = *crop; crop = NULL; } } else if (source->entity == isp->sensor.entity) { /* If crop is not requested, reset the crop rectangle on the * sensor to its default value. */ source->source.crop.width = ifmt->width; source->source.crop.height = ifmt->height; } /* Try to force the output format code onto the source pad. */ ret = v4l2_subdev_get_format(pad->entity, &format, pad->index, V4L2_SUBDEV_FORMAT_TRY); if (ret < 0) { printf("error: get format failed on %s:%u.\n", pad->entity->info.name, pad->index); return ret; } if (sink->last) format = *ofmt; else format.code = ofmt->code; ret = v4l2_subdev_set_format(pad->entity, &format, pad->index, V4L2_SUBDEV_FORMAT_TRY); if (ret < 0) { printf("error: set format failed on %s:%u.\n", pad->entity->info.name, pad->index); return ret; } if (sink->last) *ofmt = format; } source->source.format = format; if (sink->type == OMAP3_ISP_ENTITY_ENTITY) { if (sink->entity == isp->ccdc.entity) { if (format_is_shiftable(format.code, ofmt->code)) format.code = ofmt->code; } /* Propagate the format to the link target. */ pad = sink->sink.link->sink; ret = v4l2_subdev_set_format(pad->entity, &format, pad->index, V4L2_SUBDEV_FORMAT_TRY); if (ret < 0) { printf("error: set format failed on %s:%u.\n", pad->entity->info.name, pad->index); return ret; } if (sink->entity == isp->resizer.entity) { ret = v4l2_subdev_get_crop(sink->entity, &pipe->resizer.zoom_min, 0, V4L2_SUBDEV_FORMAT_TRY); if (ret < 0) printf("error: failed to get minimum zoom limit, " "zoom will be disabled.\n"); else pipe->resizer.entity = sink; } } else if (sink->type == OMAP3_ISP_ENTITY_VIDEO) { video = to_omap3_isp_video(sink); /* Set the capture format on the capture or output video * node. If the connected source is a pool, v4l2_fmt * already contains the format that has been setup on * the associated capture video node. */ if (source->type != OMAP3_ISP_ENTITY_POOL) { memset(&v4l2_fmt, 0, sizeof v4l2_fmt); v4l2_fmt.pixelformat = mbus_to_pix(format.code); v4l2_fmt.width = format.width; v4l2_fmt.height = format.height; } ret = v4l2_set_format(video->video, &v4l2_fmt); if (ret < 0) { printf("error: set format failed on %s.\n", sink->entity->info.name); return ret; } format.code = pix_to_mbus(v4l2_fmt.pixelformat); format.width = v4l2_fmt.width; format.height = v4l2_fmt.height; video->format = format; } sink->sink.format = format; source = sink; } if (pipe->resizer.entity) { pipe->resizer.zoom_max.left = pipe->resizer.zoom_min.left + pipe->resizer.zoom_min.width / 2; pipe->resizer.zoom_max.top = pipe->resizer.zoom_min.top + pipe->resizer.zoom_min.height / 2; pipe->resizer.zoom_max.width = 0; pipe->resizer.zoom_max.height = 0; ret = v4l2_subdev_set_crop(isp->resizer.entity, &pipe->resizer.zoom_max, 0, V4L2_SUBDEV_FORMAT_TRY); if (ret < 0) { printf("error: failed to get maximum zoom limit, " "zoom will be disabled.\n"); pipe->resizer.entity = NULL; } } return 0; } static int omap3_isp_pipeline_activate(struct omap3_isp_device *isp, struct omap3_isp_pipeline *pipe) { struct v4l2_mbus_framefmt format; struct omap3_isp_entity *source = NULL; struct omap3_isp_entity *sink; struct media_entity_pad *pad; bool first = true; int ret; ret = media_reset_links(isp->mdev); if (ret < 0) return ret; list_for_each_entry(sink, &pipe->entities, list) { if (source == NULL) { format = sink->source.format; source = sink; continue; } /* Enable the link. */ if (source->type != OMAP3_ISP_ENTITY_POOL && sink->type != OMAP3_ISP_ENTITY_POOL) { ret = setup_link(isp, source->entity, sink->entity, MEDIA_LNK_FL_ENABLED); if (ret < 0) return ret; } /* Configure formats. * * HACK: The number of columns cropped by the preview engine for * its internal operations requirements depend on whether the * CCDC to preview engine link is enabled or not. This can lead * to inconsistent results if the link state is changed between * trying formats on the pipeline and applying them. For this * reason, instead of blindly applying formats that have been * tried earlier, repropagate them through the pipeline. */ if (source->type == OMAP3_ISP_ENTITY_ENTITY) { pad = source->source.link->source; if (source->source.crop.width && source->source.crop.height) { struct v4l2_rect crop = source->source.crop; ret = v4l2_subdev_set_crop(pad->entity, &crop, pad->index, V4L2_SUBDEV_FORMAT_ACTIVE); if (ret < 0 && ret != -ENOTTY && ret != -EINVAL) { printf("error: set crop failed on %s:%u.\n", pad->entity->info.name, pad->index); return ret; } } if (sink->last || first) { format = source->source.format; } else { ret = v4l2_subdev_get_format(pad->entity, &format, pad->index, V4L2_SUBDEV_FORMAT_ACTIVE); if (ret < 0) { printf("error: get format failed on %s:%u.\n", pad->entity->info.name, pad->index); return ret; } format.code = source->source.format.code; } ret = v4l2_subdev_set_format(pad->entity, &format, pad->index, V4L2_SUBDEV_FORMAT_ACTIVE); if (ret < 0) { printf("error: set format failed on %s:%u.\n", pad->entity->info.name, pad->index); return ret; } } if (sink->type == OMAP3_ISP_ENTITY_ENTITY) { if (sink->entity == isp->ccdc.entity) format.code = sink->sink.format.code; /* Propagate the format to the link target. */ pad = sink->sink.link->sink; ret = v4l2_subdev_set_format(pad->entity, &format, pad->index, V4L2_SUBDEV_FORMAT_ACTIVE); if (ret < 0) { printf("error: set format failed on %s:%u.\n", pad->entity->info.name, pad->index); return ret; } } source = sink; first = false; } return 0; } static int omap3_isp_pipeline_put_buffer(struct omap3_isp_device *isp, struct omap3_isp_pipeline *pipe, void(*callback)(void*), struct v4l2_video_buffer *buffer) { if (!pipe->output->running) return 0; if (pipe->output->queued == 0) isp->ops->watch_fd(pipe->output->video->fd, OMAP3_ISP_EVENT_READ, callback, isp); return omap3_isp_video_queue_buffer(pipe->output, buffer); } static int omap3_isp_pipeline_pan_zoom(struct omap3_isp_device *isp, struct omap3_isp_pipeline *pipe, float pan_x, float pan_y, float zoom) { struct v4l2_mbus_framefmt *format; struct v4l2_rect rect; float delta; int ret; if (pipe->resizer.entity == NULL) return -ENODEV; format = &pipe->resizer.entity->sink.format; /* A x4.0 zoom level results in SBL overflows at the resizer output. */ zoom = clamp(zoom, 1.0, 3.8); delta = pipe->resizer.zoom_min.width - pipe->resizer.zoom_max.width; rect.width = format->width - (zoom - 1.0) / 3.0 * delta; rect.left = (format->width - rect.width) * pan_x; delta = pipe->resizer.zoom_min.height - pipe->resizer.zoom_max.height; rect.height = format->height - (zoom - 1.0) / 3.0 * delta; rect.top = (format->height - rect.height) * pan_y; if (pipe->resizer.entity->sink.crop.left == rect.left && pipe->resizer.entity->sink.crop.top == rect.top && pipe->resizer.entity->sink.crop.width == rect.width && pipe->resizer.entity->sink.crop.height == rect.height) return 0; ret = v4l2_subdev_set_crop(isp->resizer.entity, &rect, 0, V4L2_SUBDEV_FORMAT_ACTIVE); if (ret < 0) return ret; pipe->resizer.entity->sink.crop = rect; return 0; } /* ----------------------------------------------------------------------------- * Open/close */ struct omap3_isp_device *omap3_isp_open(const char *devname, const struct omap3_isp_operations *ops) { struct omap3_isp_device *isp; struct media_entity *entity; unsigned int i; int ret; isp = malloc(sizeof *isp); if (isp == NULL) return NULL; memset(isp, 0, sizeof *isp); isp->state = OMAP3_ISP_IDLE; isp->ops = ops; omap3_isp_pipeline_init(&isp->viewfinder); omap3_isp_pipeline_init(&isp->snapshot); omap3_isp_pipeline_init(&isp->scaler); /* Open the media device and reset all links to make sure we're in a * consistent, known state. */ isp->mdev = media_open(devname, 0); if (isp->mdev == NULL) { printf("error: unable to open media device %s\n", devname); goto error; } ret = media_reset_links(isp->mdev); if (ret < 0) { printf("error: unable to reset links.\n"); goto error; } /* Locate the entities that will be used in the pipelines. OMAP3 ISP * modules are looked up by name. */ isp->ccdc.entity = media_get_entity_by_name(isp->mdev, ENTITY_CCDC); isp->preview.entity = media_get_entity_by_name(isp->mdev, ENTITY_PREVIEW); isp->resizer.entity = media_get_entity_by_name(isp->mdev, ENTITY_RESIZER); if (isp->ccdc.entity == NULL || isp->preview.entity == NULL || isp->resizer.entity == NULL) { printf("error: unable to locate one or more ISP entities.\n"); goto error; } /* The sensor and video nodes are located by following links. */ for (i = 0; i < isp->ccdc.entity->num_links; ++i) { entity = isp->ccdc.entity->links[i].source->entity; if (media_entity_type(entity) == MEDIA_ENT_T_V4L2_SUBDEV && entity->info.pads == 1) break; } if (i == isp->ccdc.entity->num_links) { printf("error: unable to locate sensor.\n"); goto error; } isp->sensor.entity = entity; ret = omap3_isp_sensor_init(isp); if (ret < 0) { printf("error: unable to initialize sensor.\n"); goto error; } ret = omap3_isp_stats_init(isp); if (ret < 0) { printf("error: unable to initialize statistics engine.\n"); goto error; } ret = omap3_isp_preview_setup(isp); if (ret < 0) { printf("error: unable to setup preview engine.\n"); goto error; } return isp; error: omap3_isp_close(isp); return NULL; } void omap3_isp_close(struct omap3_isp_device *isp) { if (isp == NULL) return; omap3_isp_stats_cleanup(isp); omap3_isp_pipeline_destroy(&isp->viewfinder); omap3_isp_pipeline_destroy(&isp->snapshot); omap3_isp_pipeline_destroy(&isp->scaler); media_close(isp->mdev); free(isp); } /* ----------------------------------------------------------------------------- * Viewfinder */ static void omap3_isp_viewfinder_event(void *priv) { struct omap3_isp_device *isp = priv; struct v4l2_video_buffer buffer; int ret; /* Dequeue the buffer */ ret = omap3_isp_video_dequeue_buffer(isp->viewfinder.output, &buffer); if (ret < 0) { printf("error: unable to dequeue buffer: %s (%d)\n", strerror(-ret), ret); return; } if (isp->viewfinder.output->queued == 0) isp->ops->unwatch_fd(isp->viewfinder.output->video->fd); isp->ops->viewfinder_ready(isp, &buffer); } static int omap3_isp_viewfinder_setup_pipeline(struct omap3_isp_device *isp) { return omap3_isp_pipeline_activate(isp, &isp->viewfinder); } int omap3_isp_viewfinder_setup(struct omap3_isp_device *isp, struct v4l2_rect *crop, struct v4l2_mbus_framefmt *ofmt) { struct omap3_isp_pool *pool; int ret; /* Build the pipeline. Capture on the CCDC output for raw formats and on * on the resizer output for YUV formats. */ switch (ofmt->code) { case V4L2_MBUS_FMT_UYVY8_1X16: case V4L2_MBUS_FMT_YUYV8_1X16: ret = omap3_isp_pipeline_build(isp, &isp->viewfinder, ":SENSOR", ENTITY_CCDC, ENTITY_PREVIEW, ENTITY_RESIZER, ENTITY_RESIZER_OUTPUT, NULL); break; default: ret = omap3_isp_pipeline_build(isp, &isp->viewfinder, ":SENSOR", ENTITY_CCDC, ENTITY_CCDC_OUTPUT, NULL); break; } if (ret < 0) { printf("error: unable to build viewfinder pipeline (%d)\n", ret); return ret; } isp->viewfinder.scaler = OMAP3_ISP_SCALER_ISP; /* Try the format. */ ret = omap3_isp_pipeline_try_format(isp, &isp->viewfinder, &isp->sensor.format, ofmt, crop); if (ret < 0) return ret; /* Allocate buffers for intermediate pools. */ list_for_each_entry(pool, &isp->viewfinder.pools, list) omap3_isp_pool_alloc_buffers(pool); return 0; } int omap3_isp_viewfinder_alloc_pool(struct omap3_isp_device *isp, unsigned int nbufs) { struct v4l2_buffers_pool *pool; int ret; pool = v4l2_buffers_pool_new(nbufs); if (pool == NULL) { printf("error: unable to allocate buffers pool for viewfinder.\n"); return -ENOMEM; } ret = v4l2_alloc_buffers(isp->viewfinder.output->video, pool, V4L2_MEMORY_MMAP); if (ret < 0) { printf("error: unable to allocate buffers for viewfinder.\n"); return ret; } isp->viewfinder.output->pool = pool; isp->viewfinder.output->queued = 0; return 0; } void omap3_isp_viewfinder_free_pool(struct omap3_isp_device *isp) { int ret; ret = v4l2_free_buffers(isp->viewfinder.output->video); if (ret < 0) printf("error: unable to free viewfinder buffers.\n"); v4l2_buffers_pool_delete(isp->viewfinder.output->pool); isp->viewfinder.output->pool = NULL; } struct v4l2_buffers_pool * omap3_isp_viewfinder_get_pool(struct omap3_isp_device *isp) { return isp->viewfinder.output->pool; } int omap3_isp_viewfinder_set_pool(struct omap3_isp_device *isp, struct v4l2_buffers_pool *pool) { int ret; isp->viewfinder.output->pool = pool; /* Allocate video buffers. */ ret = v4l2_alloc_buffers(isp->viewfinder.output->video, pool, V4L2_MEMORY_USERPTR); if (ret < 0) { printf("error: unable to allocate buffers for viewfinder.\n"); return ret; } isp->viewfinder.output->queued = 0; return 0; } int omap3_isp_viewfinder_set_scaler(struct omap3_isp_device *isp, enum omap3_isp_scaler scaler) { struct v4l2_mbus_framefmt format; struct omap3_isp_pool *pool; int ret; if (isp->viewfinder.scaler == scaler) return 0; isp->viewfinder.scaler = scaler; /* If omap3_isp_viewfinder_setup() hasn't been called yet return now. */ if (isp->viewfinder.output->format.width == 0 || isp->viewfinder.output->format.height == 0) return 0; /* Free the intermediate pools buffers. */ list_for_each_entry(pool, &isp->viewfinder.pools, list) omap3_isp_pool_free_buffers(pool); /* Try the format. */ format = isp->viewfinder.output->format; ret = omap3_isp_pipeline_try_format(isp, &isp->viewfinder, &isp->sensor.format, &format, NULL); if (ret < 0) return ret; /* Setup the pipeline. */ ret = omap3_isp_pipeline_activate(isp, &isp->viewfinder); if (ret < 0) return ret; /* Allocate buffers for intermediate pools. */ list_for_each_entry(pool, &isp->viewfinder.pools, list) omap3_isp_pool_alloc_buffers(pool); return 0; } int omap3_isp_viewfinder_put_buffer(struct omap3_isp_device *isp, struct v4l2_video_buffer *buffer) { return omap3_isp_pipeline_put_buffer(isp, &isp->viewfinder, omap3_isp_viewfinder_event, buffer); } int omap3_isp_viewfinder_start(struct omap3_isp_device *isp, unsigned int bufs) { struct v4l2_video_buffer buffer; struct omap3_isp_pool *pool; unsigned int i; int ret; if (isp->state != OMAP3_ISP_IDLE) return -EBUSY; /* Setup the pipeline. */ ret = omap3_isp_viewfinder_setup_pipeline(isp); if (ret < 0) return ret; /* Queue buffers for video capture. */ for (i = 0; i < isp->viewfinder.output->video->nbufs; ++i) { /* Don't queue buffer that are under control of the application. */ if (!(bufs & (1 << i))) continue; buffer.index = i; ret = omap3_isp_video_queue_buffer(isp->viewfinder.output, &buffer); if (ret < 0) { printf("error: unable to queue buffer %u (%d)\n", i, ret); return ret; } } if (isp->ops->prepare_streamon) isp->ops->prepare_streamon(isp); /* Start the pools. */ list_for_each_entry(pool, &isp->viewfinder.pools, list) omap3_isp_pool_start(pool); /* Watch the viewfinder file descriptor. */ if (isp->viewfinder.output->queued) isp->ops->watch_fd(isp->viewfinder.output->video->fd, OMAP3_ISP_EVENT_READ, omap3_isp_viewfinder_event, isp); /* Start the statistics engine. */ omap3_isp_stats_start(isp); ret = omap3_isp_video_start(isp->viewfinder.output); if (ret < 0) { printf("error: streamon failed for viewfinder (%d)\n", ret); return ret; } isp->viewfinder.state = OMAP3_ISP_PIPELINE_RUNNING; isp->state = OMAP3_ISP_VIEWFINDER; return 0; } int omap3_isp_viewfinder_stop(struct omap3_isp_device *isp) { struct omap3_isp_pool *pool; int ret; isp->ops->unwatch_fd(isp->viewfinder.output->video->fd); omap3_isp_stats_stop(isp); ret = omap3_isp_video_stop(isp->viewfinder.output); if (ret < 0) { printf("error: streamoff failed for viewfinder\n"); return ret; } /* Stop the pools. */ list_for_each_entry(pool, &isp->viewfinder.pools, list) omap3_isp_pool_stop(pool); isp->viewfinder.state = OMAP3_ISP_PIPELINE_STOPPED; isp->state = OMAP3_ISP_IDLE; return 0; } int omap3_isp_viewfinder_pan_zoom(struct omap3_isp_device *isp, float x, float y, float zoom) { return omap3_isp_pipeline_pan_zoom(isp, &isp->viewfinder, x, y, zoom); } /* ----------------------------------------------------------------------------- * Snapshot * * Snapshot capture is optional. Applications using snapshot capture must call * omap3_isp_snapshot_setup() before starting the viewfinder. */ int omap3_isp_snapshot_setup(struct omap3_isp_device *isp, struct v4l2_rect *crop, struct v4l2_mbus_framefmt *ofmt) { struct v4l2_video_buffer buffer; unsigned int i; int ret; /* Build the pipeline. Capture on the CCDC output for raw formats and on * on the resizer output for YUV formats. */ switch (ofmt->code) { case V4L2_MBUS_FMT_UYVY8_1X16: case V4L2_MBUS_FMT_YUYV8_1X16: ret = omap3_isp_pipeline_build(isp, &isp->snapshot, ":SENSOR", ENTITY_CCDC, ENTITY_PREVIEW, ENTITY_RESIZER, ENTITY_RESIZER_OUTPUT, NULL); break; default: ret = omap3_isp_pipeline_build(isp, &isp->snapshot, ":SENSOR", ENTITY_CCDC, ENTITY_CCDC_OUTPUT, NULL); break; } if (ret < 0) { printf("error: unable to build snapshot pipeline (%d)\n", ret); return ret; } isp->snapshot.scaler = OMAP3_ISP_SCALER_ISP; /* Try the format. */ ret = omap3_isp_pipeline_try_format(isp, &isp->snapshot, &isp->sensor.format, ofmt, crop); if (ret < 0) return ret; /* Pre-allocate capture buffers. */ isp->snapshot.output->pool = v4l2_buffers_pool_new(2); if (isp->snapshot.output->pool == NULL) { printf("error: unable to allocate buffers pool for snapshot.\n"); return -ENOMEM; } ret = v4l2_alloc_buffers(isp->snapshot.output->video, isp->snapshot.output->pool, V4L2_MEMORY_MMAP); if (ret < 0) { printf("error: unable to allocate buffers for snapshot.\n"); return ret; } /* Queue all buffers. */ for (i = 0; i < isp->snapshot.output->video->nbufs; ++i) { buffer.index = i; ret = omap3_isp_video_queue_buffer(isp->snapshot.output, &buffer); if (ret < 0) { printf("error: unable to queue buffer %u\n", i); return ret; } } return 0; } static void omap3_isp_snapshot_event(void *priv) { struct omap3_isp_device *isp = priv; struct v4l2_video_buffer buffer; int ret; /* Dequeue a buffer and fire the snapshot event. */ ret = omap3_isp_video_dequeue_buffer(isp->snapshot.output, &buffer); if (ret < 0) { printf("error: unable to dequeue snapshot buffer.\n"); return; } if (isp->snapshot.output->queued == 0) isp->ops->unwatch_fd(isp->snapshot.output->video->fd); isp->ops->snapshot_ready(isp, &buffer); } int omap3_isp_snapshot_capture(struct omap3_isp_device *isp) { int ret; if (isp->state != OMAP3_ISP_IDLE) return -EBUSY; /* Configure the pipeline. */ ret = omap3_isp_pipeline_activate(isp, &isp->snapshot); if (ret < 0) { printf("error: unable to setup snapshot pipeline.\n"); return ret; } if (isp->ops->prepare_streamon) isp->ops->prepare_streamon(isp); /* Watch the snapshot file descriptor. */ isp->ops->watch_fd(isp->snapshot.output->video->fd, OMAP3_ISP_EVENT_READ, omap3_isp_snapshot_event, isp); /* Start the statistics engine. */ omap3_isp_stats_start(isp); ret = omap3_isp_video_start(isp->snapshot.output); if (ret < 0) { printf("error: streamon failed for snapshot\n"); return ret; } isp->state = OMAP3_ISP_SNAPSHOT; return 0; } int omap3_isp_snapshot_done(struct omap3_isp_device *isp) { struct v4l2_video_buffer buffer; unsigned int i; int ret; /* We're done, stop the snapshot stream. */ isp->ops->unwatch_fd(isp->snapshot.output->video->fd); omap3_isp_stats_stop(isp); ret = omap3_isp_video_stop(isp->snapshot.output); if (ret < 0) { printf("error: streamoff failed for snapshot\n"); return ret; } isp->state = OMAP3_ISP_IDLE; /* Queue all buffers for the next snapshot. */ for (i = 0; i < isp->snapshot.output->video->nbufs; ++i) { buffer.index = i; ret = omap3_isp_video_queue_buffer(isp->snapshot.output, &buffer); if (ret < 0) { printf("error: unable to queue buffer %u\n", i); return ret; } } return 0; } int omap3_isp_snapshot_put_buffer(struct omap3_isp_device *isp, struct v4l2_video_buffer *buffer) { return omap3_isp_pipeline_put_buffer(isp, &isp->snapshot, omap3_isp_snapshot_event, buffer); } /* ----------------------------------------------------------------------------- * Scaler */ int omap3_isp_scaler_setup(struct omap3_isp_device *isp, struct v4l2_mbus_framefmt *ifmt, struct v4l2_mbus_framefmt *ofmt) { struct v4l2_mbus_framefmt __ifmt; struct v4l2_mbus_framefmt __ofmt; int ret; /* Build the pipeline. Include the preview engine if the input format is * not a YUV format. */ switch (ifmt->code) { case V4L2_MBUS_FMT_UYVY8_1X16: case V4L2_MBUS_FMT_YUYV8_1X16: ret = omap3_isp_pipeline_build(isp, &isp->scaler, ENTITY_RESIZER_INPUT, ENTITY_RESIZER, ENTITY_RESIZER_OUTPUT, NULL); break; default: ret = omap3_isp_pipeline_build(isp, &isp->scaler, ENTITY_PREVIEW_INPUT, ENTITY_PREVIEW, ENTITY_RESIZER, ENTITY_RESIZER_OUTPUT, NULL); break; } if (ret < 0) { printf("error: unable to build scaler pipeline (%d)\n", ret); return ret; } isp->scaler.scaler = OMAP3_ISP_SCALER_ISP; /* Try the formats and make sure the results match the requirements * exactly. */ __ifmt = *ifmt; __ofmt = *ofmt; ret = omap3_isp_pipeline_try_format(isp, &isp->scaler, ifmt, ofmt, NULL); if (ret < 0) return ret; if (ifmt->width != __ifmt.width || ifmt->height != __ifmt.height || ofmt->width != __ofmt.width || ofmt->height != __ofmt.height) return -EINVAL; /* Create a buffer pool for input buffers. Every buffer in the input * pool will copy the size and reference the memory of the buffer to be * scaled. */ isp->scaler.input->pool = v4l2_buffers_pool_new(2); if (isp->scaler.input->pool == NULL) { printf("error: unable to allocate input buffers for scaler.\n"); return -ENOMEM; } return 0; } int omap3_isp_scaler_set_pool(struct omap3_isp_device *isp, struct v4l2_buffers_pool *pool) { int ret; isp->scaler.output->pool = pool; /* Allocate video buffers. */ ret = v4l2_alloc_buffers(isp->scaler.output->video, pool, V4L2_MEMORY_USERPTR); if (ret < 0) { printf("error: unable to allocate output buffers for scaler.\n"); return ret; } isp->scaler.output->queued = 0; return 0; } static void omap3_isp_scaler_input_event(void *priv) { struct omap3_isp_device *isp = priv; struct v4l2_video_buffer buffer; int ret; /* Dequeue a buffer and requeue it immediately. */ ret = omap3_isp_video_dequeue_buffer(isp->scaler.input, &buffer); if (ret < 0) { printf("error: unable to dequeue scaler input buffer.\n"); return; } buffer.bytesused = isp->scaler.input->pool->buffers[buffer.index].size; ret = omap3_isp_video_queue_buffer(isp->scaler.input, &buffer); if (ret < 0) { printf("error: unable to queue scaler input buffer.\n"); return; } } static void omap3_isp_scaler_output_event(void *priv) { struct omap3_isp_device *isp = priv; struct v4l2_video_buffer buffer; int ret; /* Dequeue the buffer */ ret = omap3_isp_video_dequeue_buffer(isp->scaler.output, &buffer); if (ret < 0) { printf("%s: unable to dequeue buffer: %s (%d)\n", __func__, strerror(-ret), ret); return; } if (isp->scaler.output->queued == 0) isp->ops->unwatch_fd(isp->scaler.output->video->fd); isp->ops->scaler_ready(isp, &buffer); } int omap3_isp_scaler_start(struct omap3_isp_device *isp, struct v4l2_video_buffer *ibuf, unsigned int obufs) { struct v4l2_video_buffer buffer; unsigned int i; int ret; /* Configure the pipeline. */ ret = omap3_isp_pipeline_activate(isp, &isp->scaler); if (ret < 0) { printf("error: unable to setup scaler pipeline.\n"); return ret; } /* Allocate input buffers. Make all buffers in the pool reference the * same memory buffer. */ for (i = 0; i < isp->scaler.input->pool->nbufs; ++i) { isp->scaler.input->pool->buffers[i].size = ibuf->size; isp->scaler.input->pool->buffers[i].mem = ibuf->mem; } ret = v4l2_alloc_buffers(isp->scaler.input->video, isp->scaler.input->pool, V4L2_MEMORY_USERPTR); if (ret < 0) { printf("error: unable to allocate input buffers for scaler.\n"); return ret; } /* Queue the input and output buffers. */ for (i = 0; i < isp->scaler.input->video->nbufs; ++i) { buffer.index = i; buffer.bytesused = ibuf->bytesused; ret = omap3_isp_video_queue_buffer(isp->scaler.input, &buffer); if (ret < 0) { printf("error: unable to queue scaler input buffer %u\n", i); return ret; } } for (i = 0; i < isp->scaler.output->video->nbufs; ++i) { /* Don't queue buffer that are under control of the application. */ if (!(obufs & (1 << i))) continue; buffer.index = i; ret = omap3_isp_video_queue_buffer(isp->scaler.output, &buffer); if (ret < 0) { printf("error: unable to queue scaler ouput buffer %u\n", i); return ret; } } /* Watch the file descriptors. */ isp->ops->watch_fd(isp->scaler.input->video->fd, OMAP3_ISP_EVENT_WRITE, omap3_isp_scaler_input_event, isp); if (isp->scaler.output->queued) isp->ops->watch_fd(isp->scaler.output->video->fd, OMAP3_ISP_EVENT_READ, omap3_isp_scaler_output_event, isp); /* Start the resizer. */ ret = omap3_isp_video_start(isp->scaler.output); if (ret < 0) { printf("error: streamon failed for scaler output\n"); return ret; } ret = omap3_isp_video_start(isp->scaler.input); if (ret < 0) { printf("error: streamon failed for scaler input\n"); return ret; } isp->state = OMAP3_ISP_SCALER; return 0; } int omap3_isp_scaler_stop(struct omap3_isp_device *isp) { int ret; isp->ops->unwatch_fd(isp->scaler.input->video->fd); isp->ops->unwatch_fd(isp->scaler.output->video->fd); /* FIXME: No sleep results in a hard system lockup. */ usleep(100000); ret = omap3_isp_video_stop(isp->scaler.input); if (ret < 0) { printf("error: streamoff failed for scaler input\n"); return ret; } ret = omap3_isp_video_stop(isp->scaler.output); if (ret < 0) { printf("error: streamoff failed for scaler output\n"); return ret; } ret = v4l2_free_buffers(isp->scaler.input->video); if (ret < 0) printf("error: unable to free scaler input buffers.\n"); isp->state = OMAP3_ISP_IDLE; return 0; } int omap3_isp_scaler_put_buffer(struct omap3_isp_device *isp, struct v4l2_video_buffer *buffer) { return omap3_isp_pipeline_put_buffer(isp, &isp->scaler, omap3_isp_scaler_output_event, buffer); } int omap3_isp_scaler_pan_zoom(struct omap3_isp_device *isp, float x, float y, float zoom) { return omap3_isp_pipeline_pan_zoom(isp, &isp->scaler, x, y, zoom); }