| /* |
| * tcm-sita.c |
| * |
| * SImple Tiler Allocator (SiTA): 2D and 1D allocation(reservation) algorithm |
| * |
| * Authors: Ravi Ramachandra <r.ramachandra@ti.com>, |
| * Lajos Molnar <molnar@ti.com> |
| * |
| * Copyright (C) 2009-2010 Texas Instruments, Inc. |
| * |
| * This package is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License version 2 as |
| * published by the Free Software Foundation. |
| * |
| * THIS PACKAGE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR |
| * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED |
| * WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. |
| * |
| */ |
| #include <linux/slab.h> |
| #include <linux/spinlock.h> |
| |
| #include "tcm-sita.h" |
| |
| #define ALIGN_DOWN(value, align) ((value) & ~((align) - 1)) |
| |
| /* Individual selection criteria for different scan areas */ |
| static s32 CR_L2R_T2B = CR_BIAS_HORIZONTAL; |
| static s32 CR_R2L_T2B = CR_DIAGONAL_BALANCE; |
| |
| /********************************************* |
| * TCM API - Sita Implementation |
| *********************************************/ |
| static s32 sita_reserve_2d(struct tcm *tcm, u16 h, u16 w, u8 align, |
| struct tcm_area *area); |
| static s32 sita_reserve_1d(struct tcm *tcm, u32 slots, struct tcm_area *area); |
| static s32 sita_free(struct tcm *tcm, struct tcm_area *area); |
| static void sita_deinit(struct tcm *tcm); |
| |
| /********************************************* |
| * Main Scanner functions |
| *********************************************/ |
| static s32 scan_areas_and_find_fit(struct tcm *tcm, u16 w, u16 h, u16 align, |
| struct tcm_area *area); |
| |
| static s32 scan_l2r_t2b(struct tcm *tcm, u16 w, u16 h, u16 align, |
| struct tcm_area *field, struct tcm_area *area); |
| |
| static s32 scan_r2l_t2b(struct tcm *tcm, u16 w, u16 h, u16 align, |
| struct tcm_area *field, struct tcm_area *area); |
| |
| static s32 scan_r2l_b2t_one_dim(struct tcm *tcm, u32 num_slots, |
| struct tcm_area *field, struct tcm_area *area); |
| |
| /********************************************* |
| * Support Infrastructure Methods |
| *********************************************/ |
| static s32 is_area_free(struct tcm_area ***map, u16 x0, u16 y0, u16 w, u16 h); |
| |
| static s32 update_candidate(struct tcm *tcm, u16 x0, u16 y0, u16 w, u16 h, |
| struct tcm_area *field, s32 criteria, |
| struct score *best); |
| |
| static void get_nearness_factor(struct tcm_area *field, |
| struct tcm_area *candidate, |
| struct nearness_factor *nf); |
| |
| static void get_neighbor_stats(struct tcm *tcm, struct tcm_area *area, |
| struct neighbor_stats *stat); |
| |
| static void fill_area(struct tcm *tcm, |
| struct tcm_area *area, struct tcm_area *parent); |
| |
| |
| /*********************************************/ |
| |
| /********************************************* |
| * Utility Methods |
| *********************************************/ |
| struct tcm *sita_init(u16 width, u16 height, struct tcm_pt *attr) |
| { |
| struct tcm *tcm; |
| struct sita_pvt *pvt; |
| struct tcm_area area = {0}; |
| s32 i; |
| |
| if (width == 0 || height == 0) |
| return NULL; |
| |
| tcm = kmalloc(sizeof(*tcm), GFP_KERNEL); |
| pvt = kmalloc(sizeof(*pvt), GFP_KERNEL); |
| if (!tcm || !pvt) |
| goto error; |
| |
| memset(tcm, 0, sizeof(*tcm)); |
| memset(pvt, 0, sizeof(*pvt)); |
| |
| /* Updating the pointers to SiTA implementation APIs */ |
| tcm->height = height; |
| tcm->width = width; |
| tcm->reserve_2d = sita_reserve_2d; |
| tcm->reserve_1d = sita_reserve_1d; |
| tcm->free = sita_free; |
| tcm->deinit = sita_deinit; |
| tcm->pvt = (void *)pvt; |
| |
| spin_lock_init(&(pvt->lock)); |
| |
| /* Creating tam map */ |
| pvt->map = kmalloc(sizeof(*pvt->map) * tcm->width, GFP_KERNEL); |
| if (!pvt->map) |
| goto error; |
| |
| for (i = 0; i < tcm->width; i++) { |
| pvt->map[i] = |
| kmalloc(sizeof(**pvt->map) * tcm->height, |
| GFP_KERNEL); |
| if (pvt->map[i] == NULL) { |
| while (i--) |
| kfree(pvt->map[i]); |
| kfree(pvt->map); |
| goto error; |
| } |
| } |
| |
| if (attr && attr->x <= tcm->width && attr->y <= tcm->height) { |
| pvt->div_pt.x = attr->x; |
| pvt->div_pt.y = attr->y; |
| |
| } else { |
| /* Defaulting to 3:1 ratio on width for 2D area split */ |
| /* Defaulting to 3:1 ratio on height for 2D and 1D split */ |
| pvt->div_pt.x = (tcm->width * 3) / 4; |
| pvt->div_pt.y = (tcm->height * 3) / 4; |
| } |
| |
| spin_lock(&(pvt->lock)); |
| assign(&area, 0, 0, width - 1, height - 1); |
| fill_area(tcm, &area, NULL); |
| spin_unlock(&(pvt->lock)); |
| return tcm; |
| |
| error: |
| kfree(tcm); |
| kfree(pvt); |
| return NULL; |
| } |
| |
| static void sita_deinit(struct tcm *tcm) |
| { |
| struct sita_pvt *pvt = (struct sita_pvt *)tcm->pvt; |
| struct tcm_area area = {0}; |
| s32 i; |
| |
| area.p1.x = tcm->width - 1; |
| area.p1.y = tcm->height - 1; |
| |
| spin_lock(&(pvt->lock)); |
| fill_area(tcm, &area, NULL); |
| spin_unlock(&(pvt->lock)); |
| |
| for (i = 0; i < tcm->height; i++) |
| kfree(pvt->map[i]); |
| kfree(pvt->map); |
| kfree(pvt); |
| } |
| |
| /** |
| * Reserve a 1D area in the container |
| * |
| * @param num_slots size of 1D area |
| * @param area pointer to the area that will be populated with the |
| * reserved area |
| * |
| * @return 0 on success, non-0 error value on failure. |
| */ |
| static s32 sita_reserve_1d(struct tcm *tcm, u32 num_slots, |
| struct tcm_area *area) |
| { |
| s32 ret; |
| struct tcm_area field = {0}; |
| struct sita_pvt *pvt = (struct sita_pvt *)tcm->pvt; |
| |
| spin_lock(&(pvt->lock)); |
| |
| /* Scanning entire container */ |
| assign(&field, tcm->width - 1, tcm->height - 1, 0, 0); |
| |
| ret = scan_r2l_b2t_one_dim(tcm, num_slots, &field, area); |
| if (!ret) |
| /* update map */ |
| fill_area(tcm, area, area); |
| |
| spin_unlock(&(pvt->lock)); |
| return ret; |
| } |
| |
| /** |
| * Reserve a 2D area in the container |
| * |
| * @param w width |
| * @param h height |
| * @param area pointer to the area that will be populated with the reserved |
| * area |
| * |
| * @return 0 on success, non-0 error value on failure. |
| */ |
| static s32 sita_reserve_2d(struct tcm *tcm, u16 h, u16 w, u8 align, |
| struct tcm_area *area) |
| { |
| s32 ret; |
| struct sita_pvt *pvt = (struct sita_pvt *)tcm->pvt; |
| |
| /* not supporting more than 64 as alignment */ |
| if (align > 64) |
| return -EINVAL; |
| |
| /* we prefer 1, 32 and 64 as alignment */ |
| align = align <= 1 ? 1 : align <= 32 ? 32 : 64; |
| |
| spin_lock(&(pvt->lock)); |
| ret = scan_areas_and_find_fit(tcm, w, h, align, area); |
| if (!ret) |
| /* update map */ |
| fill_area(tcm, area, area); |
| |
| spin_unlock(&(pvt->lock)); |
| return ret; |
| } |
| |
| /** |
| * Unreserve a previously allocated 2D or 1D area |
| * @param area area to be freed |
| * @return 0 - success |
| */ |
| static s32 sita_free(struct tcm *tcm, struct tcm_area *area) |
| { |
| struct sita_pvt *pvt = (struct sita_pvt *)tcm->pvt; |
| |
| spin_lock(&(pvt->lock)); |
| |
| /* check that this is in fact an existing area */ |
| WARN_ON(pvt->map[area->p0.x][area->p0.y] != area || |
| pvt->map[area->p1.x][area->p1.y] != area); |
| |
| /* Clear the contents of the associated tiles in the map */ |
| fill_area(tcm, area, NULL); |
| |
| spin_unlock(&(pvt->lock)); |
| |
| return 0; |
| } |
| |
| /** |
| * Note: In general the cordinates in the scan field area relevant to the can |
| * sweep directions. The scan origin (e.g. top-left corner) will always be |
| * the p0 member of the field. Therfore, for a scan from top-left p0.x <= p1.x |
| * and p0.y <= p1.y; whereas, for a scan from bottom-right p1.x <= p0.x and p1.y |
| * <= p0.y |
| */ |
| |
| /** |
| * Raster scan horizontally right to left from top to bottom to find a place for |
| * a 2D area of given size inside a scan field. |
| * |
| * @param w width of desired area |
| * @param h height of desired area |
| * @param align desired area alignment |
| * @param area pointer to the area that will be set to the best position |
| * @param field area to scan (inclusive) |
| * |
| * @return 0 on success, non-0 error value on failure. |
| */ |
| static s32 scan_r2l_t2b(struct tcm *tcm, u16 w, u16 h, u16 align, |
| struct tcm_area *field, struct tcm_area *area) |
| { |
| s32 x, y; |
| s16 start_x, end_x, start_y, end_y, found_x = -1; |
| struct tcm_area ***map = ((struct sita_pvt *)tcm->pvt)->map; |
| struct score best = {{0}, {0}, {0}, 0}; |
| |
| start_x = field->p0.x; |
| end_x = field->p1.x; |
| start_y = field->p0.y; |
| end_y = field->p1.y; |
| |
| /* check scan area co-ordinates */ |
| if (field->p0.x < field->p1.x || |
| field->p1.y < field->p0.y) |
| return -EINVAL; |
| |
| /* check if allocation would fit in scan area */ |
| if (w > LEN(start_x, end_x) || h > LEN(end_y, start_y)) |
| return -ENOSPC; |
| |
| /* adjust start_x and end_y, as allocation would not fit beyond */ |
| start_x = ALIGN_DOWN(start_x - w + 1, align); /* - 1 to be inclusive */ |
| end_y = end_y - h + 1; |
| |
| /* check if allocation would still fit in scan area */ |
| if (start_x < end_x) |
| return -ENOSPC; |
| |
| /* scan field top-to-bottom, right-to-left */ |
| for (y = start_y; y <= end_y; y++) { |
| for (x = start_x; x >= end_x; x -= align) { |
| if (is_area_free(map, x, y, w, h)) { |
| found_x = x; |
| |
| /* update best candidate */ |
| if (update_candidate(tcm, x, y, w, h, field, |
| CR_R2L_T2B, &best)) |
| goto done; |
| |
| /* change upper x bound */ |
| end_x = x + 1; |
| break; |
| } else if (map[x][y] && map[x][y]->is2d) { |
| /* step over 2D areas */ |
| x = ALIGN(map[x][y]->p0.x - w + 1, align); |
| } |
| } |
| |
| /* break if you find a free area shouldering the scan field */ |
| if (found_x == start_x) |
| break; |
| } |
| |
| if (!best.a.tcm) |
| return -ENOSPC; |
| done: |
| assign(area, best.a.p0.x, best.a.p0.y, best.a.p1.x, best.a.p1.y); |
| return 0; |
| } |
| |
| /** |
| * Raster scan horizontally left to right from top to bottom to find a place for |
| * a 2D area of given size inside a scan field. |
| * |
| * @param w width of desired area |
| * @param h height of desired area |
| * @param align desired area alignment |
| * @param area pointer to the area that will be set to the best position |
| * @param field area to scan (inclusive) |
| * |
| * @return 0 on success, non-0 error value on failure. |
| */ |
| static s32 scan_l2r_t2b(struct tcm *tcm, u16 w, u16 h, u16 align, |
| struct tcm_area *field, struct tcm_area *area) |
| { |
| s32 x, y; |
| s16 start_x, end_x, start_y, end_y, found_x = -1; |
| struct tcm_area ***map = ((struct sita_pvt *)tcm->pvt)->map; |
| struct score best = {{0}, {0}, {0}, 0}; |
| |
| start_x = field->p0.x; |
| end_x = field->p1.x; |
| start_y = field->p0.y; |
| end_y = field->p1.y; |
| |
| /* check scan area co-ordinates */ |
| if (field->p1.x < field->p0.x || |
| field->p1.y < field->p0.y) |
| return -EINVAL; |
| |
| /* check if allocation would fit in scan area */ |
| if (w > LEN(end_x, start_x) || h > LEN(end_y, start_y)) |
| return -ENOSPC; |
| |
| start_x = ALIGN(start_x, align); |
| |
| /* check if allocation would still fit in scan area */ |
| if (w > LEN(end_x, start_x)) |
| return -ENOSPC; |
| |
| /* adjust end_x and end_y, as allocation would not fit beyond */ |
| end_x = end_x - w + 1; /* + 1 to be inclusive */ |
| end_y = end_y - h + 1; |
| |
| /* scan field top-to-bottom, left-to-right */ |
| for (y = start_y; y <= end_y; y++) { |
| for (x = start_x; x <= end_x; x += align) { |
| if (is_area_free(map, x, y, w, h)) { |
| found_x = x; |
| |
| /* update best candidate */ |
| if (update_candidate(tcm, x, y, w, h, field, |
| CR_L2R_T2B, &best)) |
| goto done; |
| /* change upper x bound */ |
| end_x = x - 1; |
| |
| break; |
| } else if (map[x][y] && map[x][y]->is2d) { |
| /* step over 2D areas */ |
| x = ALIGN_DOWN(map[x][y]->p1.x, align); |
| } |
| } |
| |
| /* break if you find a free area shouldering the scan field */ |
| if (found_x == start_x) |
| break; |
| } |
| |
| if (!best.a.tcm) |
| return -ENOSPC; |
| done: |
| assign(area, best.a.p0.x, best.a.p0.y, best.a.p1.x, best.a.p1.y); |
| return 0; |
| } |
| |
| /** |
| * Raster scan horizontally right to left from bottom to top to find a place |
| * for a 1D area of given size inside a scan field. |
| * |
| * @param num_slots size of desired area |
| * @param align desired area alignment |
| * @param area pointer to the area that will be set to the best |
| * position |
| * @param field area to scan (inclusive) |
| * |
| * @return 0 on success, non-0 error value on failure. |
| */ |
| static s32 scan_r2l_b2t_one_dim(struct tcm *tcm, u32 num_slots, |
| struct tcm_area *field, struct tcm_area *area) |
| { |
| s32 found = 0; |
| s16 x, y; |
| struct sita_pvt *pvt = (struct sita_pvt *)tcm->pvt; |
| struct tcm_area *p; |
| |
| /* check scan area co-ordinates */ |
| if (field->p0.y < field->p1.y) |
| return -EINVAL; |
| |
| /** |
| * Currently we only support full width 1D scan field, which makes sense |
| * since 1D slot-ordering spans the full container width. |
| */ |
| if (tcm->width != field->p0.x - field->p1.x + 1) |
| return -EINVAL; |
| |
| /* check if allocation would fit in scan area */ |
| if (num_slots > tcm->width * LEN(field->p0.y, field->p1.y)) |
| return -ENOSPC; |
| |
| x = field->p0.x; |
| y = field->p0.y; |
| |
| /* find num_slots consecutive free slots to the left */ |
| while (found < num_slots) { |
| if (y < 0) |
| return -ENOSPC; |
| |
| /* remember bottom-right corner */ |
| if (found == 0) { |
| area->p1.x = x; |
| area->p1.y = y; |
| } |
| |
| /* skip busy regions */ |
| p = pvt->map[x][y]; |
| if (p) { |
| /* move to left of 2D areas, top left of 1D */ |
| x = p->p0.x; |
| if (!p->is2d) |
| y = p->p0.y; |
| |
| /* start over */ |
| found = 0; |
| } else { |
| /* count consecutive free slots */ |
| found++; |
| if (found == num_slots) |
| break; |
| } |
| |
| /* move to the left */ |
| if (x == 0) |
| y--; |
| x = (x ? : tcm->width) - 1; |
| |
| } |
| |
| /* set top-left corner */ |
| area->p0.x = x; |
| area->p0.y = y; |
| return 0; |
| } |
| |
| /** |
| * Find a place for a 2D area of given size inside a scan field based on its |
| * alignment needs. |
| * |
| * @param w width of desired area |
| * @param h height of desired area |
| * @param align desired area alignment |
| * @param area pointer to the area that will be set to the best position |
| * |
| * @return 0 on success, non-0 error value on failure. |
| */ |
| static s32 scan_areas_and_find_fit(struct tcm *tcm, u16 w, u16 h, u16 align, |
| struct tcm_area *area) |
| { |
| s32 ret = 0; |
| struct tcm_area field = {0}; |
| u16 boundary_x, boundary_y; |
| struct sita_pvt *pvt = (struct sita_pvt *)tcm->pvt; |
| |
| if (align > 1) { |
| /* prefer top-left corner */ |
| boundary_x = pvt->div_pt.x - 1; |
| boundary_y = pvt->div_pt.y - 1; |
| |
| /* expand width and height if needed */ |
| if (w > pvt->div_pt.x) |
| boundary_x = tcm->width - 1; |
| if (h > pvt->div_pt.y) |
| boundary_y = tcm->height - 1; |
| |
| assign(&field, 0, 0, boundary_x, boundary_y); |
| ret = scan_l2r_t2b(tcm, w, h, align, &field, area); |
| |
| /* scan whole container if failed, but do not scan 2x */ |
| if (ret != 0 && (boundary_x != tcm->width - 1 || |
| boundary_y != tcm->height - 1)) { |
| /* scan the entire container if nothing found */ |
| assign(&field, 0, 0, tcm->width - 1, tcm->height - 1); |
| ret = scan_l2r_t2b(tcm, w, h, align, &field, area); |
| } |
| } else if (align == 1) { |
| /* prefer top-right corner */ |
| boundary_x = pvt->div_pt.x; |
| boundary_y = pvt->div_pt.y - 1; |
| |
| /* expand width and height if needed */ |
| if (w > (tcm->width - pvt->div_pt.x)) |
| boundary_x = 0; |
| if (h > pvt->div_pt.y) |
| boundary_y = tcm->height - 1; |
| |
| assign(&field, tcm->width - 1, 0, boundary_x, boundary_y); |
| ret = scan_r2l_t2b(tcm, w, h, align, &field, area); |
| |
| /* scan whole container if failed, but do not scan 2x */ |
| if (ret != 0 && (boundary_x != 0 || |
| boundary_y != tcm->height - 1)) { |
| /* scan the entire container if nothing found */ |
| assign(&field, tcm->width - 1, 0, 0, tcm->height - 1); |
| ret = scan_r2l_t2b(tcm, w, h, align, &field, |
| area); |
| } |
| } |
| |
| return ret; |
| } |
| |
| /* check if an entire area is free */ |
| static s32 is_area_free(struct tcm_area ***map, u16 x0, u16 y0, u16 w, u16 h) |
| { |
| u16 x = 0, y = 0; |
| for (y = y0; y < y0 + h; y++) { |
| for (x = x0; x < x0 + w; x++) { |
| if (map[x][y]) |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /* fills an area with a parent tcm_area */ |
| static void fill_area(struct tcm *tcm, struct tcm_area *area, |
| struct tcm_area *parent) |
| { |
| s32 x, y; |
| struct sita_pvt *pvt = (struct sita_pvt *)tcm->pvt; |
| struct tcm_area a, a_; |
| |
| /* set area's tcm; otherwise, enumerator considers it invalid */ |
| area->tcm = tcm; |
| |
| tcm_for_each_slice(a, *area, a_) { |
| for (x = a.p0.x; x <= a.p1.x; ++x) |
| for (y = a.p0.y; y <= a.p1.y; ++y) |
| pvt->map[x][y] = parent; |
| |
| } |
| } |
| |
| /** |
| * Compares a candidate area to the current best area, and if it is a better |
| * fit, it updates the best to this one. |
| * |
| * @param x0, y0, w, h top, left, width, height of candidate area |
| * @param field scan field |
| * @param criteria scan criteria |
| * @param best best candidate and its scores |
| * |
| * @return 1 (true) if the candidate area is known to be the final best, so no |
| * more searching should be performed |
| */ |
| static s32 update_candidate(struct tcm *tcm, u16 x0, u16 y0, u16 w, u16 h, |
| struct tcm_area *field, s32 criteria, |
| struct score *best) |
| { |
| struct score me; /* score for area */ |
| |
| /* |
| * NOTE: For horizontal bias we always give the first found, because our |
| * scan is horizontal-raster-based and the first candidate will always |
| * have the horizontal bias. |
| */ |
| bool first = criteria & CR_BIAS_HORIZONTAL; |
| |
| assign(&me.a, x0, y0, x0 + w - 1, y0 + h - 1); |
| |
| /* calculate score for current candidate */ |
| if (!first) { |
| get_neighbor_stats(tcm, &me.a, &me.n); |
| me.neighs = me.n.edge + me.n.busy; |
| get_nearness_factor(field, &me.a, &me.f); |
| } |
| |
| /* the 1st candidate is always the best */ |
| if (!best->a.tcm) |
| goto better; |
| |
| BUG_ON(first); |
| |
| /* diagonal balance check */ |
| if ((criteria & CR_DIAGONAL_BALANCE) && |
| best->neighs <= me.neighs && |
| (best->neighs < me.neighs || |
| /* this implies that neighs and occupied match */ |
| best->n.busy < me.n.busy || |
| (best->n.busy == me.n.busy && |
| /* check the nearness factor */ |
| best->f.x + best->f.y > me.f.x + me.f.y))) |
| goto better; |
| |
| /* not better, keep going */ |
| return 0; |
| |
| better: |
| /* save current area as best */ |
| memcpy(best, &me, sizeof(me)); |
| best->a.tcm = tcm; |
| return first; |
| } |
| |
| /** |
| * Calculate the nearness factor of an area in a search field. The nearness |
| * factor is smaller if the area is closer to the search origin. |
| */ |
| static void get_nearness_factor(struct tcm_area *field, struct tcm_area *area, |
| struct nearness_factor *nf) |
| { |
| /** |
| * Using signed math as field coordinates may be reversed if |
| * search direction is right-to-left or bottom-to-top. |
| */ |
| nf->x = (s32)(area->p0.x - field->p0.x) * 1000 / |
| (field->p1.x - field->p0.x); |
| nf->y = (s32)(area->p0.y - field->p0.y) * 1000 / |
| (field->p1.y - field->p0.y); |
| } |
| |
| /* get neighbor statistics */ |
| static void get_neighbor_stats(struct tcm *tcm, struct tcm_area *area, |
| struct neighbor_stats *stat) |
| { |
| s16 x = 0, y = 0; |
| struct sita_pvt *pvt = (struct sita_pvt *)tcm->pvt; |
| |
| /* Clearing any exisiting values */ |
| memset(stat, 0, sizeof(*stat)); |
| |
| /* process top & bottom edges */ |
| for (x = area->p0.x; x <= area->p1.x; x++) { |
| if (area->p0.y == 0) |
| stat->edge++; |
| else if (pvt->map[x][area->p0.y - 1]) |
| stat->busy++; |
| |
| if (area->p1.y == tcm->height - 1) |
| stat->edge++; |
| else if (pvt->map[x][area->p1.y + 1]) |
| stat->busy++; |
| } |
| |
| /* process left & right edges */ |
| for (y = area->p0.y; y <= area->p1.y; ++y) { |
| if (area->p0.x == 0) |
| stat->edge++; |
| else if (pvt->map[area->p0.x - 1][y]) |
| stat->busy++; |
| |
| if (area->p1.x == tcm->width - 1) |
| stat->edge++; |
| else if (pvt->map[area->p1.x + 1][y]) |
| stat->busy++; |
| } |
| } |