Go to most recent revision | Details | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
4358 | Serge | 1 | /* |
2 | * Copyright 2012 Red Hat Inc. |
||
3 | * |
||
4 | * Permission is hereby granted, free of charge, to any person obtaining a |
||
5 | * copy of this software and associated documentation files (the "Software"), |
||
6 | * to deal in the Software without restriction, including without limitation |
||
7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, |
||
8 | * and/or sell copies of the Software, and to permit persons to whom the |
||
9 | * Software is furnished to do so, subject to the following conditions: |
||
10 | * |
||
11 | * The above copyright notice and this permission notice shall be included in |
||
12 | * all copies or substantial portions of the Software. |
||
13 | * |
||
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
||
17 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR |
||
18 | * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, |
||
19 | * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR |
||
20 | * OTHER DEALINGS IN THE SOFTWARE. |
||
21 | * |
||
22 | * Authors: Ben Skeggs |
||
23 | * |
||
24 | */ |
||
25 | |||
26 | #include "util/u_format.h" |
||
27 | #include "util/u_inlines.h" |
||
28 | #include "util/u_surface.h" |
||
29 | |||
30 | #include "nouveau/nv_m2mf.xml.h" |
||
31 | #include "nv30_screen.h" |
||
32 | #include "nv30_context.h" |
||
33 | #include "nv30_resource.h" |
||
34 | #include "nv30_transfer.h" |
||
35 | |||
36 | static INLINE unsigned |
||
37 | layer_offset(struct pipe_resource *pt, unsigned level, unsigned layer) |
||
38 | { |
||
39 | struct nv30_miptree *mt = nv30_miptree(pt); |
||
40 | struct nv30_miptree_level *lvl = &mt->level[level]; |
||
41 | |||
42 | if (pt->target == PIPE_TEXTURE_CUBE) |
||
43 | return (layer * mt->layer_size) + lvl->offset; |
||
44 | |||
45 | return lvl->offset + (layer * lvl->zslice_size); |
||
46 | } |
||
47 | |||
48 | static boolean |
||
49 | nv30_miptree_get_handle(struct pipe_screen *pscreen, |
||
50 | struct pipe_resource *pt, |
||
51 | struct winsys_handle *handle) |
||
52 | { |
||
53 | struct nv30_miptree *mt = nv30_miptree(pt); |
||
54 | unsigned stride; |
||
55 | |||
56 | if (!mt || !mt->base.bo) |
||
57 | return FALSE; |
||
58 | |||
59 | stride = mt->level[0].pitch; |
||
60 | |||
61 | return nouveau_screen_bo_get_handle(pscreen, mt->base.bo, stride, handle); |
||
62 | } |
||
63 | |||
64 | static void |
||
65 | nv30_miptree_destroy(struct pipe_screen *pscreen, struct pipe_resource *pt) |
||
66 | { |
||
67 | struct nv30_miptree *mt = nv30_miptree(pt); |
||
68 | |||
69 | nouveau_bo_ref(NULL, &mt->base.bo); |
||
70 | FREE(mt); |
||
71 | } |
||
72 | |||
73 | struct nv30_transfer { |
||
74 | struct pipe_transfer base; |
||
75 | struct nv30_rect img; |
||
76 | struct nv30_rect tmp; |
||
77 | unsigned nblocksx; |
||
78 | unsigned nblocksy; |
||
79 | }; |
||
80 | |||
81 | static INLINE struct nv30_transfer * |
||
82 | nv30_transfer(struct pipe_transfer *ptx) |
||
83 | { |
||
84 | return (struct nv30_transfer *)ptx; |
||
85 | } |
||
86 | |||
87 | static INLINE void |
||
88 | define_rect(struct pipe_resource *pt, unsigned level, unsigned z, |
||
89 | unsigned x, unsigned y, unsigned w, unsigned h, |
||
90 | struct nv30_rect *rect) |
||
91 | { |
||
92 | struct nv30_miptree *mt = nv30_miptree(pt); |
||
93 | struct nv30_miptree_level *lvl = &mt->level[level]; |
||
94 | |||
95 | rect->w = u_minify(pt->width0, level) << mt->ms_x; |
||
96 | rect->w = util_format_get_nblocksx(pt->format, rect->w); |
||
97 | rect->h = u_minify(pt->height0, level) << mt->ms_y; |
||
98 | rect->h = util_format_get_nblocksy(pt->format, rect->h); |
||
99 | rect->d = 1; |
||
100 | rect->z = 0; |
||
101 | if (mt->swizzled) { |
||
102 | if (pt->target == PIPE_TEXTURE_3D) { |
||
103 | rect->d = u_minify(pt->depth0, level); |
||
104 | rect->z = z; z = 0; |
||
105 | } |
||
106 | rect->pitch = 0; |
||
107 | } else { |
||
108 | rect->pitch = lvl->pitch; |
||
109 | } |
||
110 | |||
111 | rect->bo = mt->base.bo; |
||
112 | rect->domain = NOUVEAU_BO_VRAM; |
||
113 | rect->offset = layer_offset(pt, level, z); |
||
114 | rect->cpp = util_format_get_blocksize(pt->format); |
||
115 | |||
116 | rect->x0 = util_format_get_nblocksx(pt->format, x) << mt->ms_x; |
||
117 | rect->y0 = util_format_get_nblocksy(pt->format, y) << mt->ms_y; |
||
118 | rect->x1 = rect->x0 + (w << mt->ms_x); |
||
119 | rect->y1 = rect->y0 + (h << mt->ms_y); |
||
120 | } |
||
121 | |||
122 | void |
||
123 | nv30_resource_copy_region(struct pipe_context *pipe, |
||
124 | struct pipe_resource *dstres, unsigned dst_level, |
||
125 | unsigned dstx, unsigned dsty, unsigned dstz, |
||
126 | struct pipe_resource *srcres, unsigned src_level, |
||
127 | const struct pipe_box *src_box) |
||
128 | { |
||
129 | struct nv30_context *nv30 = nv30_context(pipe); |
||
130 | struct nv30_rect src, dst; |
||
131 | |||
132 | if (dstres->target == PIPE_BUFFER && srcres->target == PIPE_BUFFER) { |
||
133 | nouveau_copy_buffer(&nv30->base, |
||
134 | nv04_resource(dstres), dstx, |
||
135 | nv04_resource(srcres), src_box->x, src_box->width); |
||
136 | return; |
||
137 | } |
||
138 | |||
139 | define_rect(srcres, src_level, src_box->z, src_box->x, src_box->y, |
||
140 | src_box->width, src_box->height, &src); |
||
141 | define_rect(dstres, dst_level, dstz, dstx, dsty, |
||
142 | src_box->width, src_box->height, &dst); |
||
143 | |||
144 | nv30_transfer_rect(nv30, NEAREST, &src, &dst); |
||
145 | } |
||
146 | |||
147 | void |
||
148 | nv30_resource_resolve(struct pipe_context *pipe, |
||
149 | const struct pipe_resolve_info *info) |
||
150 | { |
||
151 | #if 0 |
||
152 | struct nv30_context *nv30 = nv30_context(pipe); |
||
153 | struct nv30_rect src, dst; |
||
154 | |||
155 | define_rect(info->src.res, 0, 0, info->src.x0, info->src.y0, |
||
156 | info->src.x1 - info->src.x0, info->src.y1 - info->src.y0, &src); |
||
157 | define_rect(info->dst.res, info->dst.level, 0, info->dst.x0, info->dst.y0, |
||
158 | info->dst.x1 - info->dst.x0, info->dst.y1 - info->dst.y0, &dst); |
||
159 | |||
160 | nv30_transfer_rect(nv30, BILINEAR, &src, &dst); |
||
161 | #endif |
||
162 | } |
||
163 | |||
164 | void |
||
165 | nv30_blit(struct pipe_context *pipe, |
||
166 | const struct pipe_blit_info *blit_info) |
||
167 | { |
||
168 | struct nv30_context *nv30 = nv30_context(pipe); |
||
169 | struct pipe_blit_info info = *blit_info; |
||
170 | |||
171 | if (info.src.resource->nr_samples > 1 && |
||
172 | info.dst.resource->nr_samples <= 1 && |
||
173 | !util_format_is_depth_or_stencil(info.src.resource->format) && |
||
174 | !util_format_is_pure_integer(info.src.resource->format)) { |
||
175 | debug_printf("nv30: color resolve unimplemented\n"); |
||
176 | return; |
||
177 | } |
||
178 | |||
179 | if (util_try_blit_via_copy_region(pipe, &info)) { |
||
180 | return; /* done */ |
||
181 | } |
||
182 | |||
183 | if (info.mask & PIPE_MASK_S) { |
||
184 | debug_printf("nv30: cannot blit stencil, skipping\n"); |
||
185 | info.mask &= ~PIPE_MASK_S; |
||
186 | } |
||
187 | |||
188 | if (!util_blitter_is_blit_supported(nv30->blitter, &info)) { |
||
189 | debug_printf("nv30: blit unsupported %s -> %s\n", |
||
190 | util_format_short_name(info.src.resource->format), |
||
191 | util_format_short_name(info.dst.resource->format)); |
||
192 | return; |
||
193 | } |
||
194 | |||
195 | /* XXX turn off occlusion queries */ |
||
196 | |||
197 | util_blitter_save_vertex_buffer_slot(nv30->blitter, nv30->vtxbuf); |
||
198 | util_blitter_save_vertex_elements(nv30->blitter, nv30->vertex); |
||
199 | util_blitter_save_vertex_shader(nv30->blitter, nv30->vertprog.program); |
||
200 | util_blitter_save_rasterizer(nv30->blitter, nv30->rast); |
||
201 | util_blitter_save_viewport(nv30->blitter, &nv30->viewport); |
||
202 | util_blitter_save_scissor(nv30->blitter, &nv30->scissor); |
||
203 | util_blitter_save_fragment_shader(nv30->blitter, nv30->fragprog.program); |
||
204 | util_blitter_save_blend(nv30->blitter, nv30->blend); |
||
205 | util_blitter_save_depth_stencil_alpha(nv30->blitter, |
||
206 | nv30->zsa); |
||
207 | util_blitter_save_stencil_ref(nv30->blitter, &nv30->stencil_ref); |
||
208 | util_blitter_save_sample_mask(nv30->blitter, nv30->sample_mask); |
||
209 | util_blitter_save_framebuffer(nv30->blitter, &nv30->framebuffer); |
||
210 | util_blitter_save_fragment_sampler_states(nv30->blitter, |
||
211 | nv30->fragprog.num_samplers, |
||
212 | (void**)nv30->fragprog.samplers); |
||
213 | util_blitter_save_fragment_sampler_views(nv30->blitter, |
||
214 | nv30->fragprog.num_textures, nv30->fragprog.textures); |
||
215 | util_blitter_save_render_condition(nv30->blitter, nv30->render_cond_query, |
||
216 | nv30->render_cond_cond, nv30->render_cond_mode); |
||
217 | util_blitter_blit(nv30->blitter, &info); |
||
218 | } |
||
219 | |||
220 | static void * |
||
221 | nv30_miptree_transfer_map(struct pipe_context *pipe, struct pipe_resource *pt, |
||
222 | unsigned level, unsigned usage, |
||
223 | const struct pipe_box *box, |
||
224 | struct pipe_transfer **ptransfer) |
||
225 | { |
||
226 | struct nv30_context *nv30 = nv30_context(pipe); |
||
227 | struct nouveau_device *dev = nv30->screen->base.device; |
||
228 | struct nv30_transfer *tx; |
||
229 | unsigned access = 0; |
||
230 | int ret; |
||
231 | |||
232 | tx = CALLOC_STRUCT(nv30_transfer); |
||
233 | if (!tx) |
||
234 | return NULL; |
||
235 | pipe_resource_reference(&tx->base.resource, pt); |
||
236 | tx->base.level = level; |
||
237 | tx->base.usage = usage; |
||
238 | tx->base.box = *box; |
||
239 | tx->base.stride = util_format_get_nblocksx(pt->format, box->width) * |
||
240 | util_format_get_blocksize(pt->format); |
||
241 | tx->base.layer_stride = util_format_get_nblocksy(pt->format, box->height) * |
||
242 | tx->base.stride; |
||
243 | |||
244 | tx->nblocksx = util_format_get_nblocksx(pt->format, box->width); |
||
245 | tx->nblocksy = util_format_get_nblocksy(pt->format, box->height); |
||
246 | |||
247 | define_rect(pt, level, box->z, box->x, box->y, |
||
248 | tx->nblocksx, tx->nblocksy, &tx->img); |
||
249 | |||
250 | ret = nouveau_bo_new(dev, NOUVEAU_BO_GART | NOUVEAU_BO_MAP, 0, |
||
251 | tx->base.layer_stride, NULL, &tx->tmp.bo); |
||
252 | if (ret) { |
||
253 | pipe_resource_reference(&tx->base.resource, NULL); |
||
254 | FREE(tx); |
||
255 | return NULL; |
||
256 | } |
||
257 | |||
258 | tx->tmp.domain = NOUVEAU_BO_GART; |
||
259 | tx->tmp.offset = 0; |
||
260 | tx->tmp.pitch = tx->base.stride; |
||
261 | tx->tmp.cpp = tx->img.cpp; |
||
262 | tx->tmp.w = tx->nblocksx; |
||
263 | tx->tmp.h = tx->nblocksy; |
||
264 | tx->tmp.d = 1; |
||
265 | tx->tmp.x0 = 0; |
||
266 | tx->tmp.y0 = 0; |
||
267 | tx->tmp.x1 = tx->tmp.w; |
||
268 | tx->tmp.y1 = tx->tmp.h; |
||
269 | tx->tmp.z = 0; |
||
270 | |||
271 | if (usage & PIPE_TRANSFER_READ) |
||
272 | nv30_transfer_rect(nv30, NEAREST, &tx->img, &tx->tmp); |
||
273 | |||
274 | if (tx->tmp.bo->map) { |
||
275 | *ptransfer = &tx->base; |
||
276 | return tx->tmp.bo->map; |
||
277 | } |
||
278 | |||
279 | if (usage & PIPE_TRANSFER_READ) |
||
280 | access |= NOUVEAU_BO_RD; |
||
281 | if (usage & PIPE_TRANSFER_WRITE) |
||
282 | access |= NOUVEAU_BO_WR; |
||
283 | |||
284 | ret = nouveau_bo_map(tx->tmp.bo, access, nv30->base.client); |
||
285 | if (ret) { |
||
286 | pipe_resource_reference(&tx->base.resource, NULL); |
||
287 | FREE(tx); |
||
288 | return NULL; |
||
289 | } |
||
290 | |||
291 | *ptransfer = &tx->base; |
||
292 | return tx->tmp.bo->map; |
||
293 | } |
||
294 | |||
295 | static void |
||
296 | nv30_miptree_transfer_unmap(struct pipe_context *pipe, |
||
297 | struct pipe_transfer *ptx) |
||
298 | { |
||
299 | struct nv30_context *nv30 = nv30_context(pipe); |
||
300 | struct nv30_transfer *tx = nv30_transfer(ptx); |
||
301 | |||
302 | if (ptx->usage & PIPE_TRANSFER_WRITE) |
||
303 | nv30_transfer_rect(nv30, NEAREST, &tx->tmp, &tx->img); |
||
304 | |||
305 | nouveau_bo_ref(NULL, &tx->tmp.bo); |
||
306 | pipe_resource_reference(&ptx->resource, NULL); |
||
307 | FREE(tx); |
||
308 | } |
||
309 | |||
310 | const struct u_resource_vtbl nv30_miptree_vtbl = { |
||
311 | nv30_miptree_get_handle, |
||
312 | nv30_miptree_destroy, |
||
313 | nv30_miptree_transfer_map, |
||
314 | u_default_transfer_flush_region, |
||
315 | nv30_miptree_transfer_unmap, |
||
316 | u_default_transfer_inline_write |
||
317 | }; |
||
318 | |||
319 | struct pipe_resource * |
||
320 | nv30_miptree_create(struct pipe_screen *pscreen, |
||
321 | const struct pipe_resource *tmpl) |
||
322 | { |
||
323 | struct nouveau_device *dev = nouveau_screen(pscreen)->device; |
||
324 | struct nv30_miptree *mt = CALLOC_STRUCT(nv30_miptree); |
||
325 | struct pipe_resource *pt = &mt->base.base; |
||
326 | unsigned blocksz, size; |
||
327 | unsigned w, h, d, l; |
||
328 | int ret; |
||
329 | |||
330 | switch (tmpl->nr_samples) { |
||
331 | case 4: |
||
332 | mt->ms_mode = 0x00004000; |
||
333 | mt->ms_x = 1; |
||
334 | mt->ms_y = 1; |
||
335 | break; |
||
336 | case 2: |
||
337 | mt->ms_mode = 0x00003000; |
||
338 | mt->ms_x = 1; |
||
339 | mt->ms_y = 0; |
||
340 | break; |
||
341 | default: |
||
342 | mt->ms_mode = 0x00000000; |
||
343 | mt->ms_x = 0; |
||
344 | mt->ms_y = 0; |
||
345 | break; |
||
346 | } |
||
347 | |||
348 | mt->base.vtbl = &nv30_miptree_vtbl; |
||
349 | *pt = *tmpl; |
||
350 | pipe_reference_init(&pt->reference, 1); |
||
351 | pt->screen = pscreen; |
||
352 | |||
353 | w = pt->width0 << mt->ms_x; |
||
354 | h = pt->height0 << mt->ms_y; |
||
355 | d = (pt->target == PIPE_TEXTURE_3D) ? pt->depth0 : 1; |
||
356 | blocksz = util_format_get_blocksize(pt->format); |
||
357 | |||
358 | if ((pt->target == PIPE_TEXTURE_RECT) || |
||
359 | !util_is_power_of_two(pt->width0) || |
||
360 | !util_is_power_of_two(pt->height0) || |
||
361 | !util_is_power_of_two(pt->depth0) || |
||
362 | util_format_is_compressed(pt->format) || |
||
363 | util_format_is_float(pt->format) || mt->ms_mode) { |
||
364 | mt->uniform_pitch = util_format_get_nblocksx(pt->format, w) * blocksz; |
||
365 | mt->uniform_pitch = align(mt->uniform_pitch, 64); |
||
366 | } |
||
367 | |||
368 | if (!mt->uniform_pitch) |
||
369 | mt->swizzled = TRUE; |
||
370 | |||
371 | size = 0; |
||
372 | for (l = 0; l <= pt->last_level; l++) { |
||
373 | struct nv30_miptree_level *lvl = &mt->level[l]; |
||
374 | unsigned nbx = util_format_get_nblocksx(pt->format, w); |
||
375 | unsigned nby = util_format_get_nblocksx(pt->format, h); |
||
376 | |||
377 | lvl->offset = size; |
||
378 | lvl->pitch = mt->uniform_pitch; |
||
379 | if (!lvl->pitch) |
||
380 | lvl->pitch = nbx * blocksz; |
||
381 | |||
382 | lvl->zslice_size = lvl->pitch * nby; |
||
383 | size += lvl->zslice_size * d; |
||
384 | |||
385 | w = u_minify(w, 1); |
||
386 | h = u_minify(h, 1); |
||
387 | d = u_minify(d, 1); |
||
388 | } |
||
389 | |||
390 | mt->layer_size = size; |
||
391 | if (pt->target == PIPE_TEXTURE_CUBE) { |
||
392 | if (!mt->uniform_pitch) |
||
393 | mt->layer_size = align(mt->layer_size, 128); |
||
394 | size = mt->layer_size * 6; |
||
395 | } |
||
396 | |||
397 | ret = nouveau_bo_new(dev, NOUVEAU_BO_VRAM, 256, size, NULL, &mt->base.bo); |
||
398 | if (ret) { |
||
399 | FREE(mt); |
||
400 | return NULL; |
||
401 | } |
||
402 | |||
403 | mt->base.domain = NOUVEAU_BO_VRAM; |
||
404 | return &mt->base.base; |
||
405 | } |
||
406 | |||
407 | struct pipe_resource * |
||
408 | nv30_miptree_from_handle(struct pipe_screen *pscreen, |
||
409 | const struct pipe_resource *tmpl, |
||
410 | struct winsys_handle *handle) |
||
411 | { |
||
412 | struct nv30_miptree *mt; |
||
413 | unsigned stride; |
||
414 | |||
415 | /* only supports 2D, non-mipmapped textures for the moment */ |
||
416 | if ((tmpl->target != PIPE_TEXTURE_2D && |
||
417 | tmpl->target != PIPE_TEXTURE_RECT) || |
||
418 | tmpl->last_level != 0 || |
||
419 | tmpl->depth0 != 1 || |
||
420 | tmpl->array_size > 1) |
||
421 | return NULL; |
||
422 | |||
423 | mt = CALLOC_STRUCT(nv30_miptree); |
||
424 | if (!mt) |
||
425 | return NULL; |
||
426 | |||
427 | mt->base.bo = nouveau_screen_bo_from_handle(pscreen, handle, &stride); |
||
428 | if (mt->base.bo == NULL) { |
||
429 | FREE(mt); |
||
430 | return NULL; |
||
431 | } |
||
432 | |||
433 | mt->base.base = *tmpl; |
||
434 | mt->base.vtbl = &nv30_miptree_vtbl; |
||
435 | pipe_reference_init(&mt->base.base.reference, 1); |
||
436 | mt->base.base.screen = pscreen; |
||
437 | mt->uniform_pitch = stride; |
||
438 | mt->level[0].pitch = mt->uniform_pitch; |
||
439 | mt->level[0].offset = 0; |
||
440 | |||
441 | /* no need to adjust bo reference count */ |
||
442 | return &mt->base.base; |
||
443 | } |
||
444 | |||
445 | struct pipe_surface * |
||
446 | nv30_miptree_surface_new(struct pipe_context *pipe, |
||
447 | struct pipe_resource *pt, |
||
448 | const struct pipe_surface *tmpl) |
||
449 | { |
||
450 | struct nv30_miptree *mt = nv30_miptree(pt); /* guaranteed */ |
||
451 | struct nv30_surface *ns; |
||
452 | struct pipe_surface *ps; |
||
453 | struct nv30_miptree_level *lvl = &mt->level[tmpl->u.tex.level]; |
||
454 | |||
455 | ns = CALLOC_STRUCT(nv30_surface); |
||
456 | if (!ns) |
||
457 | return NULL; |
||
458 | ps = &ns->base; |
||
459 | |||
460 | pipe_reference_init(&ps->reference, 1); |
||
461 | pipe_resource_reference(&ps->texture, pt); |
||
462 | ps->context = pipe; |
||
463 | ps->format = tmpl->format; |
||
464 | ps->u.tex.level = tmpl->u.tex.level; |
||
465 | ps->u.tex.first_layer = tmpl->u.tex.first_layer; |
||
466 | ps->u.tex.last_layer = tmpl->u.tex.last_layer; |
||
467 | |||
468 | ns->width = u_minify(pt->width0, ps->u.tex.level); |
||
469 | ns->height = u_minify(pt->height0, ps->u.tex.level); |
||
470 | ns->depth = ps->u.tex.last_layer - ps->u.tex.first_layer + 1; |
||
471 | ns->offset = layer_offset(pt, ps->u.tex.level, ps->u.tex.first_layer); |
||
472 | if (mt->swizzled) |
||
473 | ns->pitch = 4096; /* random, just something the hw won't reject.. */ |
||
474 | else |
||
475 | ns->pitch = lvl->pitch; |
||
476 | |||
477 | /* comment says there are going to be removed, but they're used by the st */ |
||
478 | ps->width = ns->width; |
||
479 | ps->height = ns->height; |
||
480 | return ps; |
||
481 | } |
||
482 | |||
483 | void |
||
484 | nv30_miptree_surface_del(struct pipe_context *pipe, struct pipe_surface *ps) |
||
485 | { |
||
486 | struct nv30_surface *ns = nv30_surface(ps); |
||
487 | |||
488 | pipe_resource_reference(&ps->texture, NULL); |
||
489 | FREE(ns); |
||
490 | }=>><>><>=>><>><>><>><>><>><> |