566 lines
16 KiB
C
566 lines
16 KiB
C
/**************************************************************************
|
|
|
|
Copyright 2000, 2001 ATI Technologies Inc., Ontario, Canada, and
|
|
VMware, Inc.
|
|
|
|
All Rights Reserved.
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining
|
|
a copy of this software and associated documentation files (the
|
|
"Software"), to deal in the Software without restriction, including
|
|
without limitation the rights to use, copy, modify, merge, publish,
|
|
distribute, sublicense, and/or sell copies of the Software, and to
|
|
permit persons to whom the Software is furnished to do so, subject to
|
|
the following conditions:
|
|
|
|
The above copyright notice and this permission notice (including the
|
|
next paragraph) shall be included in all copies or substantial
|
|
portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
IN NO EVENT SHALL THE COPYRIGHT OWNER(S) AND/OR ITS SUPPLIERS BE
|
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
**************************************************************************/
|
|
|
|
/*
|
|
* Authors:
|
|
* Keith Whitwell <keithw@vmware.com>
|
|
*/
|
|
|
|
#include "main/glheader.h"
|
|
#include "main/mtypes.h"
|
|
#include "main/light.h"
|
|
#include "main/enums.h"
|
|
#include "main/state.h"
|
|
|
|
#include "util/macros.h"
|
|
|
|
#include "vbo/vbo.h"
|
|
#include "tnl/tnl.h"
|
|
#include "tnl/t_pipeline.h"
|
|
|
|
#include "radeon_common.h"
|
|
#include "radeon_context.h"
|
|
#include "radeon_state.h"
|
|
#include "radeon_ioctl.h"
|
|
#include "radeon_tcl.h"
|
|
#include "radeon_swtcl.h"
|
|
#include "radeon_maos.h"
|
|
#include "radeon_common_context.h"
|
|
|
|
|
|
|
|
/*
|
|
* Render unclipped vertex buffers by emitting vertices directly to
|
|
* dma buffers. Use strip/fan hardware primitives where possible.
|
|
* Try to simulate missing primitives with indexed vertices.
|
|
*/
|
|
#define HAVE_POINTS 1
|
|
#define HAVE_LINES 1
|
|
#define HAVE_LINE_LOOP 0
|
|
#define HAVE_LINE_STRIPS 1
|
|
#define HAVE_TRIANGLES 1
|
|
#define HAVE_TRI_STRIPS 1
|
|
#define HAVE_TRI_FANS 1
|
|
#define HAVE_QUADS 0
|
|
#define HAVE_QUAD_STRIPS 0
|
|
#define HAVE_POLYGONS 1
|
|
#define HAVE_ELTS 1
|
|
|
|
|
|
#define HW_POINTS RADEON_CP_VC_CNTL_PRIM_TYPE_POINT
|
|
#define HW_LINES RADEON_CP_VC_CNTL_PRIM_TYPE_LINE
|
|
#define HW_LINE_LOOP 0
|
|
#define HW_LINE_STRIP RADEON_CP_VC_CNTL_PRIM_TYPE_LINE_STRIP
|
|
#define HW_TRIANGLES RADEON_CP_VC_CNTL_PRIM_TYPE_TRI_LIST
|
|
#define HW_TRIANGLE_STRIP_0 RADEON_CP_VC_CNTL_PRIM_TYPE_TRI_STRIP
|
|
#define HW_TRIANGLE_STRIP_1 0
|
|
#define HW_TRIANGLE_FAN RADEON_CP_VC_CNTL_PRIM_TYPE_TRI_FAN
|
|
#define HW_QUADS 0
|
|
#define HW_QUAD_STRIP 0
|
|
#define HW_POLYGON RADEON_CP_VC_CNTL_PRIM_TYPE_TRI_FAN
|
|
|
|
|
|
static GLboolean discrete_prim[0x10] = {
|
|
0, /* 0 none */
|
|
1, /* 1 points */
|
|
1, /* 2 lines */
|
|
0, /* 3 line_strip */
|
|
1, /* 4 tri_list */
|
|
0, /* 5 tri_fan */
|
|
0, /* 6 tri_type2 */
|
|
1, /* 7 rect list (unused) */
|
|
1, /* 8 3vert point */
|
|
1, /* 9 3vert line */
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
};
|
|
|
|
|
|
#define LOCAL_VARS r100ContextPtr rmesa = R100_CONTEXT(ctx)
|
|
#define ELT_TYPE GLushort
|
|
|
|
#define ELT_INIT(prim, hw_prim) \
|
|
radeonTclPrimitive( ctx, prim, hw_prim | RADEON_CP_VC_CNTL_PRIM_WALK_IND )
|
|
|
|
#define GET_MESA_ELTS() rmesa->tcl.Elts
|
|
|
|
|
|
/* Don't really know how many elts will fit in what's left of cmdbuf,
|
|
* as there is state to emit, etc:
|
|
*/
|
|
|
|
/* Testing on isosurf shows a maximum around here. Don't know if it's
|
|
* the card or driver or kernel module that is causing the behaviour.
|
|
*/
|
|
#define GET_MAX_HW_ELTS() 300
|
|
|
|
|
|
#define RESET_STIPPLE() do { \
|
|
RADEON_STATECHANGE( rmesa, lin ); \
|
|
radeonEmitState(&rmesa->radeon); \
|
|
} while (0)
|
|
|
|
#define AUTO_STIPPLE( mode ) do { \
|
|
RADEON_STATECHANGE( rmesa, lin ); \
|
|
if (mode) \
|
|
rmesa->hw.lin.cmd[LIN_RE_LINE_PATTERN] |= \
|
|
RADEON_LINE_PATTERN_AUTO_RESET; \
|
|
else \
|
|
rmesa->hw.lin.cmd[LIN_RE_LINE_PATTERN] &= \
|
|
~RADEON_LINE_PATTERN_AUTO_RESET; \
|
|
radeonEmitState(&rmesa->radeon); \
|
|
} while (0)
|
|
|
|
|
|
|
|
#define ALLOC_ELTS(nr) radeonAllocElts( rmesa, nr )
|
|
|
|
static GLushort *radeonAllocElts( r100ContextPtr rmesa, GLuint nr )
|
|
{
|
|
if (rmesa->radeon.dma.flush)
|
|
rmesa->radeon.dma.flush( &rmesa->radeon.glCtx );
|
|
|
|
radeonEmitAOS( rmesa,
|
|
rmesa->radeon.tcl.aos_count, 0 );
|
|
|
|
return radeonAllocEltsOpenEnded( rmesa, rmesa->tcl.vertex_format,
|
|
rmesa->tcl.hw_primitive, nr );
|
|
}
|
|
|
|
#define CLOSE_ELTS() if (0) RADEON_NEWPRIM( rmesa )
|
|
|
|
|
|
|
|
/* TODO: Try to extend existing primitive if both are identical,
|
|
* discrete and there are no intervening state changes. (Somewhat
|
|
* duplicates changes to DrawArrays code)
|
|
*/
|
|
static void radeonEmitPrim( struct gl_context *ctx,
|
|
GLenum prim,
|
|
GLuint hwprim,
|
|
GLuint start,
|
|
GLuint count)
|
|
{
|
|
r100ContextPtr rmesa = R100_CONTEXT( ctx );
|
|
radeonTclPrimitive( ctx, prim, hwprim );
|
|
|
|
radeonEmitAOS( rmesa,
|
|
rmesa->radeon.tcl.aos_count,
|
|
start );
|
|
|
|
/* Why couldn't this packet have taken an offset param?
|
|
*/
|
|
radeonEmitVbufPrim( rmesa,
|
|
rmesa->tcl.vertex_format,
|
|
rmesa->tcl.hw_primitive,
|
|
count - start );
|
|
}
|
|
|
|
#define EMIT_PRIM( ctx, prim, hwprim, start, count ) do { \
|
|
radeonEmitPrim( ctx, prim, hwprim, start, count ); \
|
|
(void) rmesa; } while (0)
|
|
|
|
#define MAX_CONVERSION_SIZE 40
|
|
|
|
/* Try & join small primitives
|
|
*/
|
|
#if 0
|
|
#define PREFER_DISCRETE_ELT_PRIM( NR, PRIM ) 0
|
|
#else
|
|
#define PREFER_DISCRETE_ELT_PRIM( NR, PRIM ) \
|
|
((NR) < 20 || \
|
|
((NR) < 40 && \
|
|
rmesa->tcl.hw_primitive == (PRIM| \
|
|
RADEON_CP_VC_CNTL_PRIM_WALK_IND| \
|
|
RADEON_CP_VC_CNTL_TCL_ENABLE)))
|
|
#endif
|
|
|
|
#ifdef MESA_BIG_ENDIAN
|
|
/* We could do without (most of) this ugliness if dest was always 32 bit word aligned... */
|
|
#define EMIT_ELT(dest, offset, x) do { \
|
|
int off = offset + ( ( (uintptr_t)dest & 0x2 ) >> 1 ); \
|
|
GLushort *des = (GLushort *)( (uintptr_t)dest & ~0x2 ); \
|
|
(des)[ off + 1 - 2 * ( off & 1 ) ] = (GLushort)(x); \
|
|
(void)rmesa; } while (0)
|
|
#else
|
|
#define EMIT_ELT(dest, offset, x) do { \
|
|
(dest)[offset] = (GLushort) (x); \
|
|
(void)rmesa; } while (0)
|
|
#endif
|
|
|
|
#define EMIT_TWO_ELTS(dest, offset, x, y) *(GLuint *)(dest+offset) = ((y)<<16)|(x);
|
|
|
|
|
|
|
|
#define TAG(x) tcl_##x
|
|
#include "tnl_dd/t_dd_dmatmp2.h"
|
|
|
|
/**********************************************************************/
|
|
/* External entrypoints */
|
|
/**********************************************************************/
|
|
|
|
void radeonEmitPrimitive( struct gl_context *ctx,
|
|
GLuint first,
|
|
GLuint last,
|
|
GLuint flags )
|
|
{
|
|
tcl_render_tab_verts[flags&PRIM_MODE_MASK]( ctx, first, last, flags );
|
|
}
|
|
|
|
void radeonEmitEltPrimitive( struct gl_context *ctx,
|
|
GLuint first,
|
|
GLuint last,
|
|
GLuint flags )
|
|
{
|
|
tcl_render_tab_elts[flags&PRIM_MODE_MASK]( ctx, first, last, flags );
|
|
}
|
|
|
|
void radeonTclPrimitive( struct gl_context *ctx,
|
|
GLenum prim,
|
|
int hw_prim )
|
|
{
|
|
r100ContextPtr rmesa = R100_CONTEXT(ctx);
|
|
GLuint se_cntl;
|
|
GLuint newprim = hw_prim | RADEON_CP_VC_CNTL_TCL_ENABLE;
|
|
|
|
radeon_prepare_render(&rmesa->radeon);
|
|
if (rmesa->radeon.NewGLState)
|
|
radeonValidateState( ctx );
|
|
|
|
if (newprim != rmesa->tcl.hw_primitive ||
|
|
!discrete_prim[hw_prim&0xf]) {
|
|
RADEON_NEWPRIM( rmesa );
|
|
rmesa->tcl.hw_primitive = newprim;
|
|
}
|
|
|
|
se_cntl = rmesa->hw.set.cmd[SET_SE_CNTL];
|
|
se_cntl &= ~RADEON_FLAT_SHADE_VTX_LAST;
|
|
|
|
if (prim == GL_POLYGON && ctx->Light.ShadeModel == GL_FLAT)
|
|
se_cntl |= RADEON_FLAT_SHADE_VTX_0;
|
|
else
|
|
se_cntl |= RADEON_FLAT_SHADE_VTX_LAST;
|
|
|
|
if (se_cntl != rmesa->hw.set.cmd[SET_SE_CNTL]) {
|
|
RADEON_STATECHANGE( rmesa, set );
|
|
rmesa->hw.set.cmd[SET_SE_CNTL] = se_cntl;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Predict total emit size for next rendering operation so there is no flush in middle of rendering
|
|
* Prediction has to aim towards the best possible value that is worse than worst case scenario
|
|
*/
|
|
static GLuint radeonEnsureEmitSize( struct gl_context * ctx , GLuint inputs )
|
|
{
|
|
r100ContextPtr rmesa = R100_CONTEXT(ctx);
|
|
TNLcontext *tnl = TNL_CONTEXT(ctx);
|
|
struct vertex_buffer *VB = &tnl->vb;
|
|
GLuint space_required;
|
|
GLuint state_size;
|
|
GLuint nr_aos = 1; /* radeonEmitArrays does always emit one */
|
|
int i;
|
|
/* list of flags that are allocating aos object */
|
|
const GLuint flags_to_check[] = {
|
|
VERT_BIT_NORMAL,
|
|
VERT_BIT_COLOR0,
|
|
VERT_BIT_COLOR1,
|
|
VERT_BIT_FOG
|
|
};
|
|
/* predict number of aos to emit */
|
|
for (i=0; i < ARRAY_SIZE(flags_to_check); ++i)
|
|
{
|
|
if (inputs & flags_to_check[i])
|
|
++nr_aos;
|
|
}
|
|
for (i = 0; i < ctx->Const.MaxTextureUnits; ++i)
|
|
{
|
|
if (inputs & VERT_BIT_TEX(i))
|
|
++nr_aos;
|
|
}
|
|
|
|
{
|
|
/* count the prediction for state size */
|
|
space_required = 0;
|
|
state_size = radeonCountStateEmitSize( &rmesa->radeon );
|
|
/* tcl may be changed in radeonEmitArrays so account for it if not dirty */
|
|
if (!rmesa->hw.tcl.dirty)
|
|
state_size += rmesa->hw.tcl.check( &rmesa->radeon.glCtx, &rmesa->hw.tcl );
|
|
/* predict size for elements */
|
|
for (i = 0; i < VB->PrimitiveCount; ++i)
|
|
{
|
|
/* If primitive.count is less than MAX_CONVERSION_SIZE
|
|
rendering code may decide convert to elts.
|
|
In that case we have to make pessimistic prediction.
|
|
and use larger of 2 paths. */
|
|
const GLuint elts = ELTS_BUFSZ(nr_aos);
|
|
const GLuint index = INDEX_BUFSZ;
|
|
const GLuint vbuf = VBUF_BUFSZ;
|
|
if (!VB->Primitive[i].count)
|
|
continue;
|
|
if ( (!VB->Elts && VB->Primitive[i].count >= MAX_CONVERSION_SIZE)
|
|
|| vbuf > index + elts)
|
|
space_required += vbuf;
|
|
else
|
|
space_required += index + elts;
|
|
space_required += VB->Primitive[i].count * 3;
|
|
space_required += AOS_BUFSZ(nr_aos);
|
|
}
|
|
space_required += SCISSOR_BUFSZ;
|
|
}
|
|
/* flush the buffer in case we need more than is left. */
|
|
if (rcommonEnsureCmdBufSpace(&rmesa->radeon, space_required, __func__))
|
|
return space_required + radeonCountStateEmitSize( &rmesa->radeon );
|
|
else
|
|
return space_required + state_size;
|
|
}
|
|
|
|
/**********************************************************************/
|
|
/* Render pipeline stage */
|
|
/**********************************************************************/
|
|
|
|
|
|
/* TCL render.
|
|
*/
|
|
static GLboolean radeon_run_tcl_render( struct gl_context *ctx,
|
|
struct tnl_pipeline_stage *stage )
|
|
{
|
|
r100ContextPtr rmesa = R100_CONTEXT(ctx);
|
|
TNLcontext *tnl = TNL_CONTEXT(ctx);
|
|
struct vertex_buffer *VB = &tnl->vb;
|
|
GLuint inputs = VERT_BIT_POS | VERT_BIT_COLOR0;
|
|
GLuint i;
|
|
GLuint emit_end;
|
|
|
|
/* TODO: separate this from the swtnl pipeline
|
|
*/
|
|
if (rmesa->radeon.TclFallback)
|
|
return GL_TRUE; /* fallback to software t&l */
|
|
|
|
if (VB->Count == 0)
|
|
return GL_FALSE;
|
|
|
|
/* NOTE: inputs != tnl->render_inputs - these are the untransformed
|
|
* inputs.
|
|
*/
|
|
if (ctx->Light.Enabled) {
|
|
inputs |= VERT_BIT_NORMAL;
|
|
}
|
|
|
|
if (_mesa_need_secondary_color(ctx)) {
|
|
inputs |= VERT_BIT_COLOR1;
|
|
}
|
|
|
|
if ( (ctx->Fog.FogCoordinateSource == GL_FOG_COORD) && ctx->Fog.Enabled ) {
|
|
inputs |= VERT_BIT_FOG;
|
|
}
|
|
|
|
for (i = 0 ; i < ctx->Const.MaxTextureUnits; i++) {
|
|
if (ctx->Texture.Unit[i]._Current) {
|
|
/* TODO: probably should not emit texture coords when texgen is enabled */
|
|
if (rmesa->TexGenNeedNormals[i]) {
|
|
inputs |= VERT_BIT_NORMAL;
|
|
}
|
|
inputs |= VERT_BIT_TEX(i);
|
|
}
|
|
}
|
|
|
|
radeonReleaseArrays( ctx, ~0 );
|
|
emit_end = radeonEnsureEmitSize( ctx, inputs )
|
|
+ rmesa->radeon.cmdbuf.cs->cdw;
|
|
radeonEmitArrays( ctx, inputs );
|
|
|
|
rmesa->tcl.Elts = VB->Elts;
|
|
|
|
for (i = 0 ; i < VB->PrimitiveCount ; i++)
|
|
{
|
|
GLuint prim = _tnl_translate_prim(&VB->Primitive[i]);
|
|
GLuint start = VB->Primitive[i].start;
|
|
GLuint length = VB->Primitive[i].count;
|
|
|
|
if (!length)
|
|
continue;
|
|
|
|
if (rmesa->tcl.Elts)
|
|
radeonEmitEltPrimitive( ctx, start, start+length, prim );
|
|
else
|
|
radeonEmitPrimitive( ctx, start, start+length, prim );
|
|
}
|
|
|
|
if (emit_end < rmesa->radeon.cmdbuf.cs->cdw)
|
|
WARN_ONCE("Rendering was %d commands larger than predicted size."
|
|
" We might overflow command buffer.\n", rmesa->radeon.cmdbuf.cs->cdw - emit_end);
|
|
|
|
return GL_FALSE; /* finished the pipe */
|
|
}
|
|
|
|
|
|
|
|
/* Initial state for tcl stage.
|
|
*/
|
|
const struct tnl_pipeline_stage _radeon_tcl_stage =
|
|
{
|
|
"radeon render",
|
|
NULL,
|
|
NULL,
|
|
NULL,
|
|
NULL,
|
|
radeon_run_tcl_render /* run */
|
|
};
|
|
|
|
|
|
|
|
/**********************************************************************/
|
|
/* Validate state at pipeline start */
|
|
/**********************************************************************/
|
|
|
|
|
|
/*-----------------------------------------------------------------------
|
|
* Manage TCL fallbacks
|
|
*/
|
|
|
|
|
|
static void transition_to_swtnl( struct gl_context *ctx )
|
|
{
|
|
r100ContextPtr rmesa = R100_CONTEXT(ctx);
|
|
TNLcontext *tnl = TNL_CONTEXT(ctx);
|
|
GLuint se_cntl;
|
|
|
|
RADEON_NEWPRIM( rmesa );
|
|
rmesa->swtcl.vertex_format = 0;
|
|
|
|
radeonChooseVertexState( ctx );
|
|
radeonChooseRenderState( ctx );
|
|
|
|
_tnl_validate_shine_tables( ctx );
|
|
|
|
tnl->Driver.NotifyMaterialChange =
|
|
_tnl_validate_shine_tables;
|
|
|
|
radeonReleaseArrays( ctx, ~0 );
|
|
|
|
se_cntl = rmesa->hw.set.cmd[SET_SE_CNTL];
|
|
se_cntl |= RADEON_FLAT_SHADE_VTX_LAST;
|
|
|
|
if (se_cntl != rmesa->hw.set.cmd[SET_SE_CNTL]) {
|
|
RADEON_STATECHANGE( rmesa, set );
|
|
rmesa->hw.set.cmd[SET_SE_CNTL] = se_cntl;
|
|
}
|
|
}
|
|
|
|
|
|
static void transition_to_hwtnl( struct gl_context *ctx )
|
|
{
|
|
r100ContextPtr rmesa = R100_CONTEXT(ctx);
|
|
TNLcontext *tnl = TNL_CONTEXT(ctx);
|
|
GLuint se_coord_fmt = rmesa->hw.set.cmd[SET_SE_COORDFMT];
|
|
|
|
se_coord_fmt &= ~(RADEON_VTX_XY_PRE_MULT_1_OVER_W0 |
|
|
RADEON_VTX_Z_PRE_MULT_1_OVER_W0 |
|
|
RADEON_VTX_W0_IS_NOT_1_OVER_W0);
|
|
se_coord_fmt |= RADEON_VTX_W0_IS_NOT_1_OVER_W0;
|
|
|
|
if ( se_coord_fmt != rmesa->hw.set.cmd[SET_SE_COORDFMT] ) {
|
|
RADEON_STATECHANGE( rmesa, set );
|
|
rmesa->hw.set.cmd[SET_SE_COORDFMT] = se_coord_fmt;
|
|
_tnl_need_projected_coords( ctx, GL_FALSE );
|
|
}
|
|
|
|
radeonUpdateMaterial( ctx );
|
|
|
|
tnl->Driver.NotifyMaterialChange = radeonUpdateMaterial;
|
|
|
|
if ( rmesa->radeon.dma.flush )
|
|
rmesa->radeon.dma.flush( &rmesa->radeon.glCtx );
|
|
|
|
rmesa->radeon.dma.flush = NULL;
|
|
rmesa->swtcl.vertex_format = 0;
|
|
|
|
// if (rmesa->swtcl.indexed_verts.buf)
|
|
// radeonReleaseDmaRegion( rmesa, &rmesa->swtcl.indexed_verts,
|
|
// __func__ );
|
|
|
|
if (RADEON_DEBUG & RADEON_FALLBACKS)
|
|
fprintf(stderr, "Radeon end tcl fallback\n");
|
|
}
|
|
|
|
static char *fallbackStrings[] = {
|
|
"Rasterization fallback",
|
|
"Unfilled triangles",
|
|
"Twosided lighting, differing materials",
|
|
"Materials in VB (maybe between begin/end)",
|
|
"Texgen unit 0",
|
|
"Texgen unit 1",
|
|
"Texgen unit 2",
|
|
"User disable",
|
|
"Fogcoord with separate specular lighting"
|
|
};
|
|
|
|
|
|
static char *getFallbackString(GLuint bit)
|
|
{
|
|
int i = 0;
|
|
while (bit > 1) {
|
|
i++;
|
|
bit >>= 1;
|
|
}
|
|
return fallbackStrings[i];
|
|
}
|
|
|
|
|
|
|
|
void radeonTclFallback( struct gl_context *ctx, GLuint bit, GLboolean mode )
|
|
{
|
|
r100ContextPtr rmesa = R100_CONTEXT(ctx);
|
|
GLuint oldfallback = rmesa->radeon.TclFallback;
|
|
|
|
if (mode) {
|
|
rmesa->radeon.TclFallback |= bit;
|
|
if (oldfallback == 0) {
|
|
if (RADEON_DEBUG & RADEON_FALLBACKS)
|
|
fprintf(stderr, "Radeon begin tcl fallback %s\n",
|
|
getFallbackString( bit ));
|
|
transition_to_swtnl( ctx );
|
|
}
|
|
}
|
|
else {
|
|
rmesa->radeon.TclFallback &= ~bit;
|
|
if (oldfallback == bit) {
|
|
if (RADEON_DEBUG & RADEON_FALLBACKS)
|
|
fprintf(stderr, "Radeon end tcl fallback %s\n",
|
|
getFallbackString( bit ));
|
|
transition_to_hwtnl( ctx );
|
|
}
|
|
}
|
|
}
|