mirror of
https://github.com/FFmpeg/FFmpeg.git
synced 2026-11-06 13:28:17 +00:00
Range coded slices were decoded and encoded by a subgroup of exactly 32 invocations, one for each of the states of a context, and devices which could not provide one were refused. The number of invocations now follows the subgroup size: 32 if the subgroup size can be set to it, and the largest subgroup size below it otherwise. Each invocation holds 32 divided by that number of the states of a context and of the bytes of the decoder's window, and lines are coded in chunks of as many samples. The encoder keeps its queue of 64 output bytes, and computes the slice CRC over as many segments. Devices whose subgroups are all wider than 32 invocations run the 32 invocations as part of one, and are no longer refused either. The ballot quantizers need 32 invocations, and are only used with them. With 32 invocations, the instructions executed per sample and the speed of both the decoder and the encoder are unchanged. Decoding and encoding were checked with 1 to 16 invocations on an RX 6900 XT, running the workgroups as partial subgroups, and decoding with the subgroups of 8 invocations of lavapipe.
530 lines
16 KiB
GLSL
530 lines
16 KiB
GLSL
/*
|
|
* FFv1 codec
|
|
*
|
|
* Copyright (c) 2024 Lynne <dev@lynne.ee>
|
|
*
|
|
* This file is part of FFmpeg.
|
|
*
|
|
* FFmpeg 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.
|
|
*
|
|
* FFmpeg 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
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with FFmpeg; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
#pragma shader_stage(compute)
|
|
#extension GL_GOOGLE_include_directive : require
|
|
|
|
#define DECODE
|
|
#ifndef GOLOMB
|
|
#define RC_SUBGROUP
|
|
#endif
|
|
#include "common.glsl"
|
|
#include "ffv1_common.glsl"
|
|
|
|
layout (set = 1, binding = 1, scalar) readonly buffer slice_order_buf {
|
|
uint32_t slice_order[];
|
|
};
|
|
layout (set = 1, binding = 2, scalar) writeonly buffer slice_status_buf {
|
|
uint32_t slice_status[];
|
|
};
|
|
layout (set = 1, binding = 4) coherent uniform uimage2D dec[];
|
|
|
|
#ifdef FLOAT
|
|
layout(set = 1, binding = 6) readonly buffer fltmap_buf {
|
|
uint fltmap[][4][65536];
|
|
};
|
|
#endif
|
|
|
|
#ifndef GOLOMB
|
|
|
|
layout (set = 1, binding = 3, scalar) buffer slice_state_buf {
|
|
uint8_t slice_rc_state[];
|
|
};
|
|
|
|
layout (constant_id = 20) const bool quant_ballot = false;
|
|
|
|
void decode_line_pcm(ivec2 sp, int w, int y, int p)
|
|
{
|
|
#ifndef RGB
|
|
if (p > 0 && p < 3) {
|
|
w = ceil_rshift(w, chroma_shift.x);
|
|
sp >>= chroma_shift;
|
|
}
|
|
#endif
|
|
|
|
for (int x = 0; x < w; x++) {
|
|
uint v = 0;
|
|
|
|
for (uint i = (rct_offset >> 1); i > 0; i >>= 1)
|
|
v |= get_rac_equi() ? i : 0;
|
|
|
|
if (gl_LocalInvocationID.x == 0)
|
|
imageStore(dec[p], sp + LADDR(ivec2(x, y)), uvec4(v));
|
|
}
|
|
}
|
|
|
|
shared int16_t quant_top[2][3][MAX_QUANT_TABLE_SIZE];
|
|
|
|
void decode_line(ivec2 sp, int w,
|
|
int y, int p, int bits, uint state_off,
|
|
uint8_t quant_table_idx, int run_index, bool ext)
|
|
{
|
|
#ifndef RGB
|
|
if (p > 0 && p < 3) {
|
|
w = ceil_rshift(w, chroma_shift.x);
|
|
sp >>= chroma_shift;
|
|
}
|
|
#endif
|
|
|
|
linecache_load(dec[p], sp, y, 0);
|
|
|
|
ivec3 top = subgroupBroadcast(get_pred_top(dec[p], sp, ivec2(0, y), 0, w,
|
|
quant_table_idx, ext), 0u);
|
|
ivec2 pr = get_pred_left(top, quant_table_idx, ext);
|
|
int c = pr[0];
|
|
int pred = pr[1];
|
|
int sgn = c < 0 ? -1 : 1;
|
|
int tl = top.y;
|
|
int l = linecache[1];
|
|
uint ctx = abs(c);
|
|
uint sbase = state_off + gl_LocalInvocationID.x;
|
|
uint soff = sbase + CONTEXT_SIZE*ctx;
|
|
uint ld[RC_K];
|
|
uint8_t adapted[RC_K];
|
|
[[unroll]] for (uint k = 0; k < RC_K; k++) {
|
|
ld[k] = slice_rc_state[soff + k*RC_LANES];
|
|
adapted[k] = uint8_t(0);
|
|
}
|
|
bool same = false;
|
|
uint row = 0;
|
|
ivec2 qthr = quant_ballot ? quant_thresh[quant_table_idx][gl_LocalInvocationID.x] : ivec2(0);
|
|
ivec2 qso = quant_ballot ? quant_scale_off[quant_table_idx] : ivec2(0);
|
|
|
|
#ifdef BAYER
|
|
int slot = p == 0 ? 0 : p > 1 ? 1 : -1;
|
|
#else
|
|
int slot = p == 0 ? 0 : p < 3 ? 1 : -1;
|
|
#endif
|
|
|
|
ivec4 tr = get_top(dec[p], sp, ivec2(min(1 + int(gl_LocalInvocationID.x), w - 1), y),
|
|
0, w, ext);
|
|
for (int x = 0; x < w; x += int(RC_LANES)) {
|
|
ivec3 tn;
|
|
if (slot >= 0) {
|
|
int tb = quant_top[slot][0][(tr[0] - tr[1]) & MAX_QUANT_TABLE_MASK] +
|
|
quant_top[slot][1][(tr[1] - tr[2]) & MAX_QUANT_TABLE_MASK];
|
|
if (ext)
|
|
tb += quant_top[slot][2][(tr[3] - tr[1]) & MAX_QUANT_TABLE_MASK];
|
|
tn = ivec3(tr[0], tr[1], tb);
|
|
} else {
|
|
tn = get_pred_top_quant(tr, quant_table_idx, ext);
|
|
}
|
|
tn.z += qso.y;
|
|
tr = get_top(dec[p], sp,
|
|
ivec2(min(x + int(RC_LANES) + 1 + int(gl_LocalInvocationID.x), w - 1), y),
|
|
0, w, ext);
|
|
int gmin = min(tn.y - tn.x, 0);
|
|
int gmax = max(tn.y - tn.x, 0);
|
|
int n = min(w - x, int(RC_LANES));
|
|
|
|
int j = 0;
|
|
do {
|
|
RCStates st;
|
|
[[unroll]] for (uint k = 0; k < RC_K; k++)
|
|
st.v[k] = same ? uint(adapted[k]) : ld[k];
|
|
int base = subgroupBroadcast(tn.z, j);
|
|
int t = subgroupBroadcast(tn.y, j);
|
|
|
|
uint used, used_bits;
|
|
int v = get_isymbol(st, pred, sgn, used, used_bits);
|
|
uint vz = zero_extend(v, bits);
|
|
#ifdef FLOAT
|
|
v = int(vz);
|
|
#endif
|
|
|
|
if (quant_ballot) {
|
|
uvec4 q0 = subgroupBallot(int(int8_t(v - tl)) >= qthr.x);
|
|
uvec4 q3 = subgroupBallot(ext && int(int8_t(l - v)) >= qthr.y);
|
|
c = base + int(subgroupBallotBitCount(q0)) + qso.x*int(subgroupBallotBitCount(q3));
|
|
} else {
|
|
c = base + quant_table[quant_table_idx][0][(v - tl) & MAX_QUANT_TABLE_MASK];
|
|
if (ext)
|
|
c += quant_table[quant_table_idx][3][(l - v) & MAX_QUANT_TABLE_MASK];
|
|
}
|
|
uint ctx_prev = ctx;
|
|
uint soff_prev = soff;
|
|
ctx = abs(c);
|
|
soff = sbase + CONTEXT_SIZE*ctx;
|
|
same = ctx == ctx_prev;
|
|
if (!same)
|
|
[[unroll]] for (uint k = 0; k < RC_K; k++)
|
|
ld[k] = slice_rc_state[soff + k*RC_LANES];
|
|
rac_renorm();
|
|
[[unroll]] for (uint k = 0; k < RC_K; k++) {
|
|
bool one = subgroupInverseBallot(uvec4(used_bits >> (k*RC_LANES), 0, 0, 0));
|
|
uint nst = zero_one_state[st.v[k] + (one ? 256 : 0)];
|
|
bool read = subgroupInverseBallot(uvec4(used >> (k*RC_LANES), 0, 0, 0));
|
|
adapted[k] = uint8_t(read ? nst : st.v[k]);
|
|
slice_rc_state[soff_prev + k*RC_LANES] = adapted[k];
|
|
}
|
|
sgn = c < 0 ? -1 : 1;
|
|
|
|
int vm = int(TYPE(vz));
|
|
pred = subgroupBroadcast(clamp(tn.y, vm + gmin, vm + gmax), j);
|
|
row = gl_LocalInvocationID.x == j ? vz : row;
|
|
rac_check_window();
|
|
|
|
l = v;
|
|
tl = t;
|
|
} while (++j < n);
|
|
|
|
if (gl_LocalInvocationID.x < n)
|
|
imageStore(dec[p], sp + LADDR(ivec2(x + int(gl_LocalInvocationID.x), y)), uvec4(row));
|
|
}
|
|
|
|
memoryBarrierImage();
|
|
barrier();
|
|
}
|
|
|
|
#else /* GOLOMB */
|
|
|
|
layout (set = 1, binding = 3, scalar) buffer slice_state_buf {
|
|
VlcState slice_vlc_state[];
|
|
};
|
|
|
|
GetBitContext gb;
|
|
|
|
void golomb_init(void)
|
|
{
|
|
if (version == 3 && micro_version > 1 || version > 3)
|
|
get_rac_internal((rc.range * 129) >> 8);
|
|
|
|
uint64_t ac_byte_count = rc.bs_off - rc.bs_start - 1;
|
|
init_get_bits(gb, u8buf(slice_data + rc.bs_start + ac_byte_count),
|
|
int(rc.bs_end - rc.bs_start - ac_byte_count));
|
|
}
|
|
|
|
void decode_line(ivec2 sp, int w,
|
|
int y, int p, int bits, uint state_off,
|
|
uint8_t quant_table_idx, inout int run_index, bool ext)
|
|
{
|
|
#ifndef RGB
|
|
if (p > 0 && p < 3) {
|
|
w = ceil_rshift(w, chroma_shift.x);
|
|
sp >>= chroma_shift;
|
|
}
|
|
#endif
|
|
|
|
linecache_load(dec[p], sp, y, 0);
|
|
|
|
int run_count = 0;
|
|
int run_mode = 0;
|
|
|
|
for (int x = 0; x < w; x++) {
|
|
ivec2 pos = sp + ivec2(x, y);
|
|
int diff;
|
|
ivec2 pr = get_pred(dec[p], sp, ivec2(x, y), 0, w,
|
|
quant_table_idx, ext);
|
|
|
|
uint vlc_off = state_off + abs(pr[0]);
|
|
|
|
if (pr[0] == 0 && run_mode == 0)
|
|
run_mode = 1;
|
|
|
|
if (run_mode != 0) {
|
|
if (run_count == 0 && run_mode == 1) {
|
|
int tmp_idx = int(log2_run[run_index]);
|
|
if (get_bit(gb)) {
|
|
run_count = 1 << tmp_idx;
|
|
if (x + run_count <= w)
|
|
run_index++;
|
|
} else {
|
|
if (tmp_idx != 0) {
|
|
run_count = int(get_bits(gb, tmp_idx));
|
|
} else
|
|
run_count = 0;
|
|
|
|
if (run_index != 0)
|
|
run_index--;
|
|
run_mode = 2;
|
|
}
|
|
}
|
|
|
|
run_count--;
|
|
if (run_count < 0) {
|
|
run_mode = 0;
|
|
run_count = 0;
|
|
diff = read_vlc_symbol(gb, slice_vlc_state[vlc_off], bits);
|
|
if (diff >= 0)
|
|
diff++;
|
|
} else {
|
|
diff = 0;
|
|
}
|
|
} else {
|
|
diff = read_vlc_symbol(gb, slice_vlc_state[vlc_off], bits);
|
|
}
|
|
|
|
if (pr[0] < 0)
|
|
diff = -diff;
|
|
|
|
uint v = zero_extend(pr[1] + diff, bits);
|
|
imageStore(dec[p], sp + LADDR(ivec2(x, y)), uvec4(v));
|
|
linecache_next(TYPE(v));
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void decode_plane_line(ivec2 sp, int w, int y, int p, int bits, uint state_off,
|
|
uint8_t quant_table_idx, inout int run_index)
|
|
{
|
|
if (has_extend_lookup && extend_lookup[quant_table_idx])
|
|
decode_line(sp, w, y, p, bits, state_off, quant_table_idx, run_index, true);
|
|
else
|
|
decode_line(sp, w, y, p, bits, state_off, quant_table_idx, run_index, false);
|
|
}
|
|
|
|
#ifdef BAYER
|
|
void writeout_bayer(uint slice_idx, in SliceContext sc, ivec2 sp, int w, int y)
|
|
{
|
|
memoryBarrierImage();
|
|
barrier();
|
|
|
|
int offset = rct_offset;
|
|
|
|
for (uint x = gl_LocalInvocationID.x; x < w; x += gl_WorkGroupSize.x) {
|
|
ivec2 lpos = sp + LADDR(ivec2(x, y));
|
|
ivec2 pos = sc.slice_pos + ivec2(int(x) << 1, y << 1);
|
|
|
|
int g_r = int(imageLoad(dec[0], lpos)[0]);
|
|
int g_b = int(imageLoad(dec[1], lpos)[0]);
|
|
int b = int(imageLoad(dec[2], lpos)[0]);
|
|
int r = int(imageLoad(dec[3], lpos)[0]);
|
|
|
|
if (sc.slice_coding_mode != 1) {
|
|
b -= offset;
|
|
r -= offset;
|
|
g_r -= (b*sc.slice_rct_coef.g + r*sc.slice_rct_coef.r) >> 2;
|
|
b += g_r;
|
|
r += g_r;
|
|
|
|
int gd = g_b - offset;
|
|
g_b = g_r - (gd >> 1);
|
|
g_r = g_b + gd;
|
|
}
|
|
|
|
imageStore(dst[0], pos + ivec2(0, 0), uvec4(r));
|
|
imageStore(dst[0], pos + ivec2(1, 0), uvec4(g_r));
|
|
imageStore(dst[0], pos + ivec2(0, 1), uvec4(g_b));
|
|
imageStore(dst[0], pos + ivec2(1, 1), uvec4(b));
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#ifdef RGB
|
|
ivec4 transform_sample(ivec4 pix, ivec2 rct_coef, int offset)
|
|
{
|
|
pix.b -= offset;
|
|
pix.r -= offset;
|
|
pix.g -= (pix.b*rct_coef.g + pix.r*rct_coef.r) >> 2;
|
|
pix.b += pix.g;
|
|
pix.r += pix.g;
|
|
return pix;
|
|
}
|
|
|
|
void writeout_rgb(uint slice_idx, in SliceContext sc, ivec2 sp, int w, int y,
|
|
bool apply_rct)
|
|
{
|
|
memoryBarrierImage();
|
|
barrier();
|
|
|
|
for (uint x = gl_LocalInvocationID.x; x < w; x += gl_WorkGroupSize.x) {
|
|
ivec2 lpos = sp + LADDR(ivec2(x, y));
|
|
ivec2 pos = sc.slice_pos + ivec2(x, y);
|
|
|
|
ivec4 pix;
|
|
pix.r = int(imageLoad(dec[2], lpos)[0]);
|
|
pix.g = int(imageLoad(dec[0], lpos)[0]);
|
|
pix.b = int(imageLoad(dec[1], lpos)[0]);
|
|
if (transparency)
|
|
pix.a = int(imageLoad(dec[3], lpos)[0]);
|
|
|
|
if (apply_rct)
|
|
#ifdef FLOAT
|
|
pix = transform_sample(pix, sc.slice_rct_coef, sc.remap_count[0]);
|
|
#else
|
|
pix = transform_sample(pix, sc.slice_rct_coef, rct_offset);
|
|
#endif
|
|
|
|
#ifdef FLOAT
|
|
pix = pix.gbra;
|
|
vec4 pd;
|
|
for (int i = 0; i < color_planes; i++) {
|
|
uint mask = (1u << ceil_log2(sc.remap_count[i])) - 1u;
|
|
uint v = fltmap[slice_idx][i][uint(pix[i]) & mask];
|
|
if (c_bits >= 32)
|
|
pd[i] = uintBitsToFloat(v);
|
|
else
|
|
pd[i] = float(uint16BitsToFloat16(uint16_t(v)));
|
|
}
|
|
pd = pd.brga;
|
|
|
|
pd = vec4(pd[fmt_lut[0]], pd[fmt_lut[1]],
|
|
pd[fmt_lut[2]], pd[fmt_lut[3]]);
|
|
#define CAST(x) vec4(x)
|
|
#else
|
|
#define CAST(x) ivec4(x)
|
|
ivec4 pd = ivec4(pix[fmt_lut[0]], pix[fmt_lut[1]],
|
|
pix[fmt_lut[2]], pix[fmt_lut[3]]);
|
|
#endif
|
|
|
|
imageStore(dst[0], pos, pd);
|
|
if (planar_rgb) {
|
|
for (int i = 1; i < color_planes; i++)
|
|
imageStore(dst[i], pos, CAST(pd[i]));
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void decode_slice(in SliceContext sc, uint slice_idx)
|
|
{
|
|
int w = sc.slice_dim.x;
|
|
ivec2 sp = sc.slice_pos;
|
|
u16vec4 bits = get_slice_bits(sc);
|
|
|
|
#ifdef BAYER
|
|
/* Bayer logical dims: 2x2 blocks at half resolution */
|
|
w >>= 1;
|
|
int bayer_h = sc.slice_dim.y >> 1;
|
|
sp.x >>= 1;
|
|
sp.y = int(slice_idx / gl_NumWorkGroups.x)*rgb_linecache;
|
|
/* c_bits = bps + 1 (the +1 is for is_rgb). For PCM mode, all planes use
|
|
* raw bps. For non-PCM, gm uses bps (bps+1 before 4.8, which coded an
|
|
* extra bit); gd/b-gm/r-gm use bps+1. */
|
|
if (sc.slice_coding_mode == 0) {
|
|
bits = u16vec4(c_bits - 1, c_bits, c_bits, c_bits);
|
|
if (version == 4 && micro_version < 8)
|
|
bits[0] = uint16_t(c_bits);
|
|
} else
|
|
bits = u16vec4(c_bits - 1, c_bits - 1, c_bits - 1, c_bits - 1);
|
|
#elif defined(RGB)
|
|
sp.y = int(slice_idx / gl_NumWorkGroups.x)*rgb_linecache;
|
|
#endif
|
|
|
|
#ifndef GOLOMB
|
|
/* PCM coding */
|
|
if (sc.slice_coding_mode == 1) {
|
|
#ifdef BAYER
|
|
for (int y = 0; y < bayer_h; y++) {
|
|
for (int p = 0; p < 4; p++)
|
|
decode_line_pcm(sp, w, y, p);
|
|
writeout_bayer(slice_idx, sc, sp, w, y);
|
|
}
|
|
#elif defined(RGB)
|
|
for (int y = 0; y < sc.slice_dim.y; y++) {
|
|
for (int p = 0; p < color_planes; p++)
|
|
decode_line_pcm(sp, w, y, p);
|
|
|
|
writeout_rgb(slice_idx, sc, sp, w, y, false);
|
|
}
|
|
#else
|
|
for (int p = 0; p < planes; p++) {
|
|
int h = sc.slice_dim.y;
|
|
if (p > 0 && p < 3)
|
|
h = ceil_rshift(h, chroma_shift.y);
|
|
|
|
for (int y = 0; y < h; y++)
|
|
decode_line_pcm(sp, w, y, p);
|
|
}
|
|
#endif
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
#ifdef BAYER
|
|
u8vec4 quant_table_idx = sc.quant_table_idx.xzyy;
|
|
u32vec4 slice_state_off = (slice_idx*codec_planes +
|
|
uvec4(0, 2, 1, 1))*plane_state_size;
|
|
#else
|
|
u8vec4 quant_table_idx = sc.quant_table_idx.xyyz;
|
|
u32vec4 slice_state_off = (slice_idx*codec_planes +
|
|
uvec4(0, 1, 1, 2))*plane_state_size;
|
|
#endif
|
|
|
|
#ifdef GOLOMB
|
|
slice_state_off >>= 3; // division by VLC_STATE_SIZE
|
|
golomb_init();
|
|
#else
|
|
for (uint i = gl_LocalInvocationID.x; i < 2*3*MAX_QUANT_TABLE_SIZE; i += gl_WorkGroupSize.x) {
|
|
uint t = i / (3*MAX_QUANT_TABLE_SIZE);
|
|
uint k = (i / MAX_QUANT_TABLE_SIZE) % 3;
|
|
uint e = i % MAX_QUANT_TABLE_SIZE;
|
|
quant_top[t][k][e] = int16_t(quant_table[sc.quant_table_idx[t]][k == 2 ? 4 : k + 1][e]);
|
|
}
|
|
barrier();
|
|
#endif
|
|
|
|
#ifdef BAYER
|
|
int run_index = 0;
|
|
for (int y = 0; y < bayer_h; y++) {
|
|
for (int p = 0; p < 4; p++)
|
|
decode_plane_line(sp, w, y, p, bits[p], slice_state_off[p],
|
|
quant_table_idx[p], run_index);
|
|
|
|
writeout_bayer(slice_idx, sc, sp, w, y);
|
|
}
|
|
#elif defined(RGB)
|
|
int run_index = 0;
|
|
for (int y = 0; y < sc.slice_dim.y; y++) {
|
|
for (int p = 0; p < color_planes; p++)
|
|
decode_plane_line(sp, w, y, p, bits[p], slice_state_off[p],
|
|
quant_table_idx[p], run_index);
|
|
|
|
writeout_rgb(slice_idx, sc, sp, w, y, true);
|
|
}
|
|
#else
|
|
for (int p = 0; p < planes; p++) {
|
|
int h = sc.slice_dim.y;
|
|
if (p > 0 && p < 3)
|
|
h = ceil_rshift(h, chroma_shift.y);
|
|
|
|
int run_index = 0;
|
|
for (int y = 0; y < h; y++)
|
|
decode_plane_line(sp, w, y, p, bits[p], slice_state_off[p],
|
|
quant_table_idx[p], run_index);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void main(void)
|
|
{
|
|
uint slice_idx = slice_order[gl_WorkGroupID.y*gl_NumWorkGroups.x + gl_WorkGroupID.x];
|
|
|
|
#ifdef GOLOMB
|
|
rc = slice_ctx[slice_idx].c;
|
|
#else
|
|
rac_init_dec(slice_ctx[slice_idx].c);
|
|
#endif
|
|
|
|
decode_slice(slice_ctx[slice_idx], slice_idx);
|
|
|
|
if (gl_LocalInvocationID.x == 0) {
|
|
#ifndef GOLOMB
|
|
rc.bs_off += rc_pos;
|
|
#endif
|
|
uint overread = 0;
|
|
if (rc.bs_off >= (rc.bs_end + MAX_OVERREAD))
|
|
overread = rc.bs_off - rc.bs_end;
|
|
slice_status[2*slice_idx + 1] = overread;
|
|
}
|
|
}
|