1258 lines
32 KiB
C
1258 lines
32 KiB
C
/*
|
|
* Copyright © 2006 Red Hat, Inc
|
|
*
|
|
* Permission to use, copy, modify, distribute, and sell this software
|
|
* and its documentation for any purpose is hereby granted without
|
|
* fee, provided that the above copyright notice appear in all copies
|
|
* and that both that copyright notice and this permission notice
|
|
* appear in supporting documentation, and that the name of Red Hat,
|
|
* Inc not be used in advertising or publicity pertaining to
|
|
* distribution of the software without specific, written prior
|
|
* permission. Red Hat, Inc makes no representations about the
|
|
* suitability of this software for any purpose. It is provided "as
|
|
* is" without express or implied warranty.
|
|
*
|
|
* RED HAT, INC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
|
|
* INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
|
|
* NO EVENT SHALL RED HAT, INC BE LIABLE FOR ANY SPECIAL, INDIRECT OR
|
|
* CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
|
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
|
|
* NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
|
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
|
|
#ifdef HAVE_DIX_CONFIG_H
|
|
#include <dix-config.h>
|
|
#endif
|
|
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <sys/time.h>
|
|
#include <dlfcn.h>
|
|
|
|
#include <drm.h>
|
|
#include <GL/gl.h>
|
|
#include <GL/internal/dri_interface.h>
|
|
|
|
#include <windowstr.h>
|
|
#include <os.h>
|
|
|
|
#define _XF86DRI_SERVER_
|
|
#include <drm_sarea.h>
|
|
#include <xf86drm.h>
|
|
#include <xf86dristr.h>
|
|
#include <xf86str.h>
|
|
#include <xf86.h>
|
|
#include <dri.h>
|
|
|
|
#define DRI_NEW_INTERFACE_ONLY
|
|
#include "glxserver.h"
|
|
#include "glxutil.h"
|
|
#include "glcontextmodes.h"
|
|
|
|
#include "g_disptab.h"
|
|
#include "glapitable.h"
|
|
#include "glapi.h"
|
|
#include "glthread.h"
|
|
#include "dispatch.h"
|
|
#include "extension_string.h"
|
|
|
|
|
|
#define STRINGIFY(macro_or_string) STRINGIFY_ARG (macro_or_string)
|
|
#define STRINGIFY_ARG(contents) #contents
|
|
|
|
typedef struct __GLXDRIscreen __GLXDRIscreen;
|
|
typedef struct __GLXDRIcontext __GLXDRIcontext;
|
|
typedef struct __GLXDRIdrawable __GLXDRIdrawable;
|
|
|
|
struct __GLXDRIscreen {
|
|
__GLXscreen base;
|
|
|
|
__DRIscreen driScreen;
|
|
void *driver;
|
|
|
|
xf86EnterVTProc *enterVT;
|
|
xf86LeaveVTProc *leaveVT;
|
|
|
|
DRITexOffsetStartProcPtr texOffsetStart;
|
|
DRITexOffsetFinishProcPtr texOffsetFinish;
|
|
__GLXpixmap* texOffsetOverride[16];
|
|
GLuint lastTexOffsetOverride;
|
|
|
|
unsigned char glx_enable_bits[__GLX_EXT_BYTES];
|
|
};
|
|
|
|
struct __GLXDRIcontext {
|
|
__GLXcontext base;
|
|
|
|
__DRIcontext driContext;
|
|
};
|
|
|
|
struct __GLXDRIdrawable {
|
|
__GLXdrawable base;
|
|
|
|
__DRIdrawable *driDrawable;
|
|
};
|
|
|
|
/* History:
|
|
* 20021121 - Initial version
|
|
* 20021128 - Added __glXWindowExists() function
|
|
* 20021207 - Added support for dynamic GLX extensions,
|
|
* GLX_SGI_swap_control, GLX_SGI_video_sync,
|
|
* GLX_OML_sync_control, and GLX_MESA_swap_control.
|
|
* Never officially released. Do NOT test against
|
|
* this version. Use 20030317 instead.
|
|
* 20030317 - Added support GLX_SGIX_fbconfig,
|
|
* GLX_MESA_swap_frame_usage, GLX_OML_swap_method,
|
|
* GLX_{ARB,SGIS}_multisample, and
|
|
* GLX_SGIX_visual_select_group.
|
|
* 20030606 - Added support for GLX_SGI_make_current_read.
|
|
* 20030813 - Made support for dynamic extensions multi-head aware.
|
|
* 20030818 - Added support for GLX_MESA_allocate_memory in place of the
|
|
* deprecated GLX_NV_vertex_array_range & GLX_MESA_agp_offset
|
|
* interfaces.
|
|
* 20031201 - Added support for the first round of DRI interface changes.
|
|
* Do NOT test against this version! It has binary
|
|
* compatibility bugs, use 20040317 instead.
|
|
* 20040317 - Added the 'mode' field to __DRIcontextRec.
|
|
* 20040415 - Added support for bindContext3 and unbindContext3.
|
|
* 20040602 - Add __glXGetDrawableInfo. I though that was there
|
|
* months ago. :(
|
|
* 20050727 - Gut all the old interfaces. This breaks compatability with
|
|
* any DRI driver built to any previous version.
|
|
* 20060314 - Added support for GLX_MESA_copy_sub_buffer.
|
|
*/
|
|
|
|
#define INTERNAL_VERSION 20050727
|
|
|
|
static const char CREATE_NEW_SCREEN_FUNC[] =
|
|
"__driCreateNewScreen_" STRINGIFY (INTERNAL_VERSION);
|
|
|
|
|
|
static void
|
|
__glXDRIleaveServer(GLboolean rendering)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; rendering && i < screenInfo.numScreens; i++) {
|
|
__GLXDRIscreen * const screen =
|
|
(__GLXDRIscreen *) __glXgetActiveScreen(i);
|
|
GLuint lastOverride = screen->lastTexOffsetOverride;
|
|
|
|
if (lastOverride) {
|
|
__GLXpixmap **texOffsetOverride = screen->texOffsetOverride;
|
|
int j;
|
|
|
|
for (j = 0; j < lastOverride; j++) {
|
|
__GLXpixmap *pGlxPix = texOffsetOverride[j];
|
|
|
|
if (pGlxPix && pGlxPix->texname) {
|
|
pGlxPix->offset =
|
|
screen->texOffsetStart((PixmapPtr)pGlxPix->pDraw);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
DRIBlockHandler(NULL, NULL, NULL);
|
|
|
|
for (i = 0; rendering && i < screenInfo.numScreens; i++) {
|
|
__GLXDRIscreen * const screen =
|
|
(__GLXDRIscreen *) __glXgetActiveScreen(i);
|
|
GLuint lastOverride = screen->lastTexOffsetOverride;
|
|
|
|
if (lastOverride) {
|
|
__GLXpixmap **texOffsetOverride = screen->texOffsetOverride;
|
|
int j;
|
|
|
|
for (j = 0; j < lastOverride; j++) {
|
|
__GLXpixmap *pGlxPix = texOffsetOverride[j];
|
|
|
|
if (pGlxPix && pGlxPix->texname) {
|
|
screen->driScreen.setTexOffset(pGlxPix->pDRICtx,
|
|
pGlxPix->texname,
|
|
pGlxPix->offset,
|
|
pGlxPix->pDraw->depth,
|
|
((PixmapPtr)pGlxPix->pDraw)->
|
|
devKind);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
__glXDRIenterServer(GLboolean rendering)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; rendering && i < screenInfo.numScreens; i++) {
|
|
__GLXDRIscreen * const screen =
|
|
(__GLXDRIscreen *) __glXgetActiveScreen(i);
|
|
|
|
if (screen->lastTexOffsetOverride) {
|
|
CALL_Flush(GET_DISPATCH(), ());
|
|
break;
|
|
}
|
|
}
|
|
|
|
DRIWakeupHandler(NULL, 0, NULL);
|
|
}
|
|
|
|
/**
|
|
* \bug
|
|
* We're jumping through hoops here to get the DRIdrawable which the DRI
|
|
* driver tries to keep to it self... cf. FIXME in \c createDrawable.
|
|
*/
|
|
static void
|
|
__glXDRIdrawableFoo(__GLXDRIdrawable *draw)
|
|
{
|
|
__GLXDRIscreen * const screen =
|
|
(__GLXDRIscreen *) __glXgetActiveScreen(draw->base.pDraw->pScreen->myNum);
|
|
|
|
draw->driDrawable = (*screen->driScreen.getDrawable)(NULL,
|
|
draw->base.drawId,
|
|
screen->driScreen.private);
|
|
}
|
|
|
|
static void
|
|
__glXDRIdrawableDestroy(__GLXdrawable *private)
|
|
{
|
|
#if 0
|
|
(*glxPriv->driDrawable.destroyDrawable)(NULL,
|
|
glxPriv->driDrawable.private);
|
|
#endif
|
|
|
|
xfree(private);
|
|
}
|
|
|
|
static GLboolean
|
|
__glXDRIdrawableResize(__GLXdrawable *glxPriv)
|
|
{
|
|
/* Nothing to do here, the DRI driver asks the server for drawable
|
|
* geometry when it sess the SAREA timestamps change.*/
|
|
|
|
return GL_TRUE;
|
|
}
|
|
|
|
static GLboolean
|
|
__glXDRIdrawableSwapBuffers(__GLXdrawable *basePrivate)
|
|
{
|
|
__GLXDRIdrawable *private = (__GLXDRIdrawable *) basePrivate;
|
|
|
|
__glXDRIdrawableFoo(private);
|
|
|
|
(*private->driDrawable->swapBuffers)(NULL,
|
|
private->driDrawable->private);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
static int
|
|
__glXDRIdrawableSwapInterval(__GLXdrawable *baseDrawable, int interval)
|
|
{
|
|
__GLXDRIdrawable *draw = (__GLXDRIdrawable *) baseDrawable;
|
|
|
|
__glXDRIdrawableFoo(draw);
|
|
|
|
draw->driDrawable->swap_interval = interval;
|
|
return 0;
|
|
}
|
|
|
|
|
|
static void
|
|
__glXDRIdrawableCopySubBuffer(__GLXdrawable *basePrivate,
|
|
int x, int y, int w, int h)
|
|
{
|
|
__GLXDRIdrawable *private = (__GLXDRIdrawable *) basePrivate;
|
|
|
|
__glXDRIdrawableFoo(private);
|
|
|
|
(*private->driDrawable->copySubBuffer)(NULL,
|
|
private->driDrawable->private,
|
|
x, y, w, h);
|
|
}
|
|
|
|
static void
|
|
__glXDRIcontextDestroy(__GLXcontext *baseContext)
|
|
{
|
|
__GLXDRIcontext *context = (__GLXDRIcontext *) baseContext;
|
|
|
|
context->driContext.destroyContext(NULL,
|
|
context->base.pScreen->myNum,
|
|
context->driContext.private);
|
|
__glXContextDestroy(&context->base);
|
|
xfree(context);
|
|
}
|
|
|
|
static int
|
|
__glXDRIcontextMakeCurrent(__GLXcontext *baseContext)
|
|
{
|
|
__GLXDRIcontext *context = (__GLXDRIcontext *) baseContext;
|
|
|
|
return (*context->driContext.bindContext)(NULL,
|
|
context->base.pScreen->myNum,
|
|
baseContext->drawPriv->drawId,
|
|
baseContext->readPriv->drawId,
|
|
&context->driContext);
|
|
}
|
|
|
|
static int
|
|
__glXDRIcontextLoseCurrent(__GLXcontext *baseContext)
|
|
{
|
|
__GLXDRIcontext *context = (__GLXDRIcontext *) baseContext;
|
|
|
|
return (*context->driContext.unbindContext)(NULL,
|
|
context->base.pScreen->myNum,
|
|
baseContext->drawPriv->drawId,
|
|
baseContext->readPriv->drawId,
|
|
&context->driContext);
|
|
}
|
|
|
|
static int
|
|
__glXDRIcontextCopy(__GLXcontext *baseDst, __GLXcontext *baseSrc,
|
|
unsigned long mask)
|
|
{
|
|
__GLXDRIcontext *dst = (__GLXDRIcontext *) baseDst;
|
|
__GLXDRIcontext *src = (__GLXDRIcontext *) baseSrc;
|
|
|
|
/* FIXME: We will need to add DRIcontext::copyContext for this. */
|
|
|
|
(void) dst;
|
|
(void) src;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static int
|
|
__glXDRIcontextForceCurrent(__GLXcontext *baseContext)
|
|
{
|
|
__GLXDRIcontext *context = (__GLXDRIcontext *) baseContext;
|
|
|
|
return (*context->driContext.bindContext)(NULL,
|
|
context->base.pScreen->myNum,
|
|
baseContext->drawPriv->drawId,
|
|
baseContext->readPriv->drawId,
|
|
&context->driContext);
|
|
}
|
|
|
|
static void
|
|
glxFillAlphaChannel (PixmapPtr pixmap, int x, int y, int width, int height)
|
|
{
|
|
int i;
|
|
CARD32 *p, *end, *pixels = (CARD32 *)pixmap->devPrivate.ptr;
|
|
CARD32 rowstride = pixmap->devKind / 4;
|
|
|
|
for (i = y; i < y + height; i++)
|
|
{
|
|
p = &pixels[i * rowstride + x];
|
|
end = p + width;
|
|
while (p < end)
|
|
*p++ |= 0xFF000000;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* (sticking this here for lack of a better place)
|
|
* Known issues with the GLX_EXT_texture_from_pixmap implementation:
|
|
* - In general we ignore the fbconfig, lots of examples follow
|
|
* - No fbconfig handling for multiple mipmap levels
|
|
* - No fbconfig handling for 1D textures
|
|
* - No fbconfig handling for TEXTURE_TARGET
|
|
* - No fbconfig exposure of Y inversion state
|
|
* - No GenerateMipmapEXT support (due to no FBO support)
|
|
* - No support for anything but 16bpp and 32bpp-sparse pixmaps
|
|
*/
|
|
|
|
static int
|
|
__glXDRIbindTexImage(__GLXcontext *baseContext,
|
|
int buffer,
|
|
__GLXpixmap *glxPixmap)
|
|
{
|
|
RegionPtr pRegion = NULL;
|
|
PixmapPtr pixmap;
|
|
int bpp, override = 0;
|
|
GLenum format, type;
|
|
ScreenPtr pScreen = glxPixmap->pScreen;
|
|
__GLXDRIscreen * const screen =
|
|
(__GLXDRIscreen *) __glXgetActiveScreen(pScreen->myNum);
|
|
|
|
pixmap = (PixmapPtr) glxPixmap->pDraw;
|
|
|
|
if (screen->texOffsetStart && screen->driScreen.setTexOffset) {
|
|
__GLXpixmap **texOffsetOverride = screen->texOffsetOverride;
|
|
int i, firstEmpty = 16, texname;
|
|
|
|
for (i = 0; i < 16; i++) {
|
|
if (texOffsetOverride[i] == glxPixmap)
|
|
goto alreadyin;
|
|
|
|
if (firstEmpty == 16 && !texOffsetOverride[i])
|
|
firstEmpty = i;
|
|
}
|
|
|
|
if (firstEmpty == 16) {
|
|
ErrorF("%s: Failed to register texture offset override\n", __func__);
|
|
goto nooverride;
|
|
}
|
|
|
|
if (firstEmpty >= screen->lastTexOffsetOverride)
|
|
screen->lastTexOffsetOverride = firstEmpty + 1;
|
|
|
|
texOffsetOverride[firstEmpty] = glxPixmap;
|
|
|
|
alreadyin:
|
|
override = 1;
|
|
|
|
glxPixmap->pDRICtx = &((__GLXDRIcontext*)baseContext)->driContext;
|
|
|
|
CALL_GetIntegerv(GET_DISPATCH(), (glxPixmap->target == GL_TEXTURE_2D ?
|
|
GL_TEXTURE_BINDING_2D :
|
|
GL_TEXTURE_BINDING_RECTANGLE_NV,
|
|
&texname));
|
|
|
|
if (texname == glxPixmap->texname)
|
|
return Success;
|
|
|
|
glxPixmap->texname = texname;
|
|
|
|
screen->driScreen.setTexOffset(glxPixmap->pDRICtx, texname, 0,
|
|
pixmap->drawable.depth, pixmap->devKind);
|
|
}
|
|
nooverride:
|
|
|
|
if (!glxPixmap->pDamage) {
|
|
if (!override) {
|
|
glxPixmap->pDamage = DamageCreate(NULL, NULL, DamageReportNone,
|
|
TRUE, pScreen, NULL);
|
|
if (!glxPixmap->pDamage)
|
|
return BadAlloc;
|
|
|
|
DamageRegister ((DrawablePtr) pixmap, glxPixmap->pDamage);
|
|
}
|
|
|
|
pRegion = NULL;
|
|
} else {
|
|
pRegion = DamageRegion(glxPixmap->pDamage);
|
|
if (REGION_NIL(pRegion))
|
|
return Success;
|
|
}
|
|
|
|
/* XXX 24bpp packed, 8, etc */
|
|
if (pixmap->drawable.depth >= 24) {
|
|
bpp = 4;
|
|
format = GL_BGRA;
|
|
type =
|
|
#if X_BYTE_ORDER == X_BIG_ENDIAN
|
|
!override ? GL_UNSIGNED_INT_8_8_8_8_REV :
|
|
#endif
|
|
GL_UNSIGNED_BYTE;
|
|
} else {
|
|
bpp = 2;
|
|
format = GL_RGB;
|
|
type = GL_UNSIGNED_SHORT_5_6_5;
|
|
}
|
|
|
|
CALL_PixelStorei( GET_DISPATCH(), (GL_UNPACK_ROW_LENGTH,
|
|
pixmap->devKind / bpp) );
|
|
|
|
if (pRegion == NULL)
|
|
{
|
|
if (!override && pixmap->drawable.depth == 24)
|
|
glxFillAlphaChannel(pixmap,
|
|
pixmap->drawable.x,
|
|
pixmap->drawable.y,
|
|
pixmap->drawable.width,
|
|
pixmap->drawable.height);
|
|
|
|
CALL_PixelStorei( GET_DISPATCH(), (GL_UNPACK_SKIP_PIXELS,
|
|
pixmap->drawable.x) );
|
|
CALL_PixelStorei( GET_DISPATCH(), (GL_UNPACK_SKIP_ROWS,
|
|
pixmap->drawable.y) );
|
|
|
|
CALL_TexImage2D( GET_DISPATCH(),
|
|
(glxPixmap->target,
|
|
0,
|
|
bpp == 4 ? 4 : 3,
|
|
pixmap->drawable.width,
|
|
pixmap->drawable.height,
|
|
0,
|
|
format,
|
|
type,
|
|
override ? NULL : pixmap->devPrivate.ptr) );
|
|
} else if (!override) {
|
|
int i, numRects;
|
|
BoxPtr p;
|
|
|
|
numRects = REGION_NUM_RECTS (pRegion);
|
|
p = REGION_RECTS (pRegion);
|
|
for (i = 0; i < numRects; i++)
|
|
{
|
|
if (pixmap->drawable.depth == 24)
|
|
glxFillAlphaChannel(pixmap,
|
|
pixmap->drawable.x + p[i].x1,
|
|
pixmap->drawable.y + p[i].y1,
|
|
p[i].x2 - p[i].x1,
|
|
p[i].y2 - p[i].y1);
|
|
|
|
CALL_PixelStorei( GET_DISPATCH(), (GL_UNPACK_SKIP_PIXELS,
|
|
pixmap->drawable.x + p[i].x1) );
|
|
CALL_PixelStorei( GET_DISPATCH(), (GL_UNPACK_SKIP_ROWS,
|
|
pixmap->drawable.y + p[i].y1) );
|
|
|
|
CALL_TexSubImage2D( GET_DISPATCH(),
|
|
(glxPixmap->target,
|
|
0,
|
|
p[i].x1, p[i].y1,
|
|
p[i].x2 - p[i].x1, p[i].y2 - p[i].y1,
|
|
format,
|
|
type,
|
|
pixmap->devPrivate.ptr) );
|
|
}
|
|
}
|
|
|
|
if (!override)
|
|
DamageEmpty(glxPixmap->pDamage);
|
|
|
|
return Success;
|
|
}
|
|
|
|
static int
|
|
__glXDRIreleaseTexImage(__GLXcontext *baseContext,
|
|
int buffer,
|
|
__GLXpixmap *pixmap)
|
|
{
|
|
ScreenPtr pScreen = pixmap->pScreen;
|
|
__GLXDRIscreen * const screen =
|
|
(__GLXDRIscreen *) __glXgetActiveScreen(pScreen->myNum);
|
|
GLuint lastOverride = screen->lastTexOffsetOverride;
|
|
|
|
if (lastOverride) {
|
|
__GLXpixmap **texOffsetOverride = screen->texOffsetOverride;
|
|
int i;
|
|
|
|
for (i = 0; i < lastOverride; i++) {
|
|
if (texOffsetOverride[i] == pixmap) {
|
|
if (screen->texOffsetFinish)
|
|
screen->texOffsetFinish((PixmapPtr)pixmap->pDraw);
|
|
|
|
texOffsetOverride[i] = NULL;
|
|
|
|
if (i + 1 == lastOverride) {
|
|
lastOverride = 0;
|
|
|
|
while (i--) {
|
|
if (texOffsetOverride[i]) {
|
|
lastOverride = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
screen->lastTexOffsetOverride = lastOverride;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return Success;
|
|
}
|
|
|
|
static __GLXtextureFromPixmap __glXDRItextureFromPixmap = {
|
|
__glXDRIbindTexImage,
|
|
__glXDRIreleaseTexImage
|
|
};
|
|
|
|
static void
|
|
__glXDRIscreenDestroy(__GLXscreen *baseScreen)
|
|
{
|
|
__GLXDRIscreen *screen = (__GLXDRIscreen *) baseScreen;
|
|
|
|
screen->driScreen.destroyScreen(NULL,
|
|
screen->base.pScreen->myNum,
|
|
screen->driScreen.private);
|
|
|
|
dlclose(screen->driver);
|
|
|
|
__glXScreenDestroy(baseScreen);
|
|
|
|
xfree(screen);
|
|
}
|
|
|
|
static __GLXcontext *
|
|
__glXDRIscreenCreateContext(__GLXscreen *baseScreen,
|
|
__GLcontextModes *modes,
|
|
__GLXcontext *baseShareContext)
|
|
{
|
|
__GLXDRIscreen *screen = (__GLXDRIscreen *) baseScreen;
|
|
__GLXDRIcontext *context, *shareContext;
|
|
void *sharePrivate;
|
|
|
|
shareContext = (__GLXDRIcontext *) baseShareContext;
|
|
if (shareContext)
|
|
sharePrivate = shareContext->driContext.private;
|
|
else
|
|
sharePrivate = NULL;
|
|
|
|
if (baseShareContext && baseShareContext->isDirect)
|
|
return NULL;
|
|
|
|
context = xalloc(sizeof *context);
|
|
if (context == NULL)
|
|
return NULL;
|
|
|
|
memset(context, 0, sizeof *context);
|
|
context->base.destroy = __glXDRIcontextDestroy;
|
|
context->base.makeCurrent = __glXDRIcontextMakeCurrent;
|
|
context->base.loseCurrent = __glXDRIcontextLoseCurrent;
|
|
context->base.copy = __glXDRIcontextCopy;
|
|
context->base.forceCurrent = __glXDRIcontextForceCurrent;
|
|
context->base.pScreen = screen->base.pScreen;
|
|
|
|
context->base.textureFromPixmap = &__glXDRItextureFromPixmap;
|
|
|
|
context->driContext.private =
|
|
screen->driScreen.createNewContext(NULL, modes,
|
|
0, /* render type */
|
|
sharePrivate,
|
|
&context->driContext);
|
|
|
|
if (!context->driContext.private) {
|
|
xfree(context);
|
|
return NULL;
|
|
}
|
|
|
|
context->driContext.mode = modes;
|
|
|
|
return &context->base;
|
|
}
|
|
|
|
static __GLXdrawable *
|
|
__glXDRIscreenCreateDrawable(__GLXscreen *screen,
|
|
DrawablePtr pDraw,
|
|
XID drawId,
|
|
__GLcontextModes *modes)
|
|
{
|
|
__GLXDRIdrawable *private;
|
|
|
|
private = xalloc(sizeof *private);
|
|
if (private == NULL)
|
|
return NULL;
|
|
|
|
memset(private, 0, sizeof *private);
|
|
|
|
if (!__glXDrawableInit(&private->base, screen, pDraw, drawId, modes)) {
|
|
xfree(private);
|
|
return NULL;
|
|
}
|
|
|
|
private->base.destroy = __glXDRIdrawableDestroy;
|
|
private->base.resize = __glXDRIdrawableResize;
|
|
private->base.swapBuffers = __glXDRIdrawableSwapBuffers;
|
|
private->base.copySubBuffer = __glXDRIdrawableCopySubBuffer;
|
|
|
|
#if 0
|
|
/* FIXME: It would only be natural that we called
|
|
* driScreen->createNewDrawable here but the DRI drivers manage
|
|
* them a little oddly. FIXME: describe this better.*/
|
|
|
|
/* The last argument is 'attrs', which is used with pbuffers which
|
|
* we currently don't support. */
|
|
|
|
glxPriv->driDrawable.private =
|
|
(screen->driScreen.createNewDrawable)(NULL, modes,
|
|
drawId,
|
|
&glxPriv->driDrawable,
|
|
0,
|
|
NULL);
|
|
#endif
|
|
|
|
return &private->base;
|
|
}
|
|
|
|
|
|
static unsigned
|
|
filter_modes(__GLcontextModes **server_modes,
|
|
const __GLcontextModes *driver_modes)
|
|
{
|
|
__GLcontextModes * m;
|
|
__GLcontextModes ** prev_next;
|
|
const __GLcontextModes * check;
|
|
unsigned modes_count = 0;
|
|
|
|
if ( driver_modes == NULL ) {
|
|
LogMessage(X_WARNING,
|
|
"AIGLX: 3D driver returned no fbconfigs.\n");
|
|
return 0;
|
|
}
|
|
|
|
/* For each mode in server_modes, check to see if a matching mode exists
|
|
* in driver_modes. If not, then the mode is not available.
|
|
*/
|
|
|
|
prev_next = server_modes;
|
|
for ( m = *prev_next ; m != NULL ; m = *prev_next ) {
|
|
GLboolean do_delete = GL_TRUE;
|
|
|
|
for ( check = driver_modes ; check != NULL ; check = check->next ) {
|
|
if ( _gl_context_modes_are_same( m, check ) ) {
|
|
do_delete = GL_FALSE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* The 3D has to support all the modes that match the GLX visuals
|
|
* sent from the X server.
|
|
*/
|
|
if ( do_delete && (m->visualID != 0) ) {
|
|
do_delete = GL_FALSE;
|
|
|
|
LogMessage(X_WARNING,
|
|
"AIGLX: 3D driver claims to not support "
|
|
"visual 0x%02x\n", m->visualID);
|
|
}
|
|
|
|
if ( do_delete ) {
|
|
*prev_next = m->next;
|
|
|
|
m->next = NULL;
|
|
_gl_context_modes_destroy( m );
|
|
}
|
|
else {
|
|
modes_count++;
|
|
prev_next = & m->next;
|
|
}
|
|
}
|
|
|
|
return modes_count;
|
|
}
|
|
|
|
|
|
static void
|
|
enable_glx_extension(void *psc, const char *ext_name)
|
|
{
|
|
__GLXDRIscreen * const screen = (__GLXDRIscreen *) psc;
|
|
|
|
__glXEnableExtension(screen->glx_enable_bits, ext_name);
|
|
}
|
|
|
|
|
|
static __DRIfuncPtr getProcAddress(const char *proc_name)
|
|
{
|
|
if (strcmp(proc_name, "glxEnableExtension") == 0) {
|
|
return (__DRIfuncPtr) enable_glx_extension;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static __DRIscreen *findScreen(__DRInativeDisplay *dpy, int scrn)
|
|
{
|
|
__GLXDRIscreen *screen = (__GLXDRIscreen *) __glXgetActiveScreen(scrn);
|
|
|
|
return &screen->driScreen;
|
|
}
|
|
|
|
static GLboolean windowExists(__DRInativeDisplay *dpy, __DRIid draw)
|
|
{
|
|
DrawablePtr pDrawable = (DrawablePtr) LookupIDByType(draw, RT_WINDOW);
|
|
int unused;
|
|
drm_clip_rect_t *pRects;
|
|
|
|
return pDrawable ? DRIGetDrawableInfo(pDrawable->pScreen, pDrawable,
|
|
(unsigned*)&unused, (unsigned*)&unused,
|
|
&unused, &unused, &unused, &unused,
|
|
&unused, &pRects, &unused, &unused,
|
|
&unused, &pRects)
|
|
: GL_FALSE;
|
|
}
|
|
|
|
static GLboolean createContext(__DRInativeDisplay *dpy, int screen,
|
|
int configID, void *contextID,
|
|
drm_context_t *hw_context)
|
|
{
|
|
XID fakeID;
|
|
VisualPtr visual;
|
|
int i;
|
|
ScreenPtr pScreen;
|
|
GLboolean retval;
|
|
|
|
pScreen = screenInfo.screens[screen];
|
|
|
|
/* Find the requested X visual */
|
|
visual = pScreen->visuals;
|
|
for (i = 0; i < pScreen->numVisuals; i++, visual++)
|
|
if (visual->vid == configID)
|
|
break;
|
|
if (i == pScreen->numVisuals)
|
|
return GL_FALSE;
|
|
|
|
fakeID = FakeClientID(0);
|
|
*(XID *) contextID = fakeID;
|
|
|
|
__glXDRIenterServer(GL_FALSE);
|
|
retval = DRICreateContext(pScreen, visual, fakeID, hw_context);
|
|
__glXDRIleaveServer(GL_FALSE);
|
|
return retval;
|
|
}
|
|
|
|
static GLboolean destroyContext(__DRInativeDisplay *dpy, int screen,
|
|
__DRIid context)
|
|
{
|
|
GLboolean retval;
|
|
|
|
__glXDRIenterServer(GL_FALSE);
|
|
retval = DRIDestroyContext(screenInfo.screens[screen], context);
|
|
__glXDRIleaveServer(GL_FALSE);
|
|
return retval;
|
|
}
|
|
|
|
static GLboolean
|
|
createDrawable(__DRInativeDisplay *dpy, int screen,
|
|
__DRIid drawable, drm_drawable_t *hHWDrawable)
|
|
{
|
|
DrawablePtr pDrawable;
|
|
GLboolean retval;
|
|
|
|
pDrawable = (DrawablePtr) LookupIDByClass(drawable, RC_DRAWABLE);
|
|
if (!pDrawable)
|
|
return GL_FALSE;
|
|
|
|
__glXDRIenterServer(GL_FALSE);
|
|
retval = DRICreateDrawable(screenInfo.screens[screen], __pGlxClient,
|
|
pDrawable, hHWDrawable);
|
|
__glXDRIleaveServer(GL_FALSE);
|
|
return retval;
|
|
}
|
|
|
|
static GLboolean
|
|
destroyDrawable(__DRInativeDisplay *dpy, int screen, __DRIid drawable)
|
|
{
|
|
DrawablePtr pDrawable;
|
|
GLboolean retval;
|
|
|
|
pDrawable = (DrawablePtr) LookupIDByClass(drawable, RC_DRAWABLE);
|
|
if (!pDrawable)
|
|
return GL_FALSE;
|
|
|
|
__glXDRIenterServer(GL_FALSE);
|
|
retval = DRIDestroyDrawable(screenInfo.screens[screen], __pGlxClient,
|
|
pDrawable);
|
|
__glXDRIleaveServer(GL_FALSE);
|
|
return retval;
|
|
}
|
|
|
|
static GLboolean
|
|
getDrawableInfo(__DRInativeDisplay *dpy, int screen,
|
|
__DRIid drawable, unsigned int *index, unsigned int *stamp,
|
|
int *x, int *y, int *width, int *height,
|
|
int *numClipRects, drm_clip_rect_t **ppClipRects,
|
|
int *backX, int *backY,
|
|
int *numBackClipRects, drm_clip_rect_t **ppBackClipRects)
|
|
{
|
|
DrawablePtr pDrawable;
|
|
drm_clip_rect_t *pClipRects, *pBackClipRects;
|
|
GLboolean retval;
|
|
size_t size;
|
|
|
|
pDrawable = (DrawablePtr) LookupIDByClass(drawable, RC_DRAWABLE);
|
|
if (!pDrawable) {
|
|
ErrorF("getDrawableInfo failed to look up window\n");
|
|
|
|
*index = 0;
|
|
*stamp = 0;
|
|
*x = 0;
|
|
*y = 0;
|
|
*width = 0;
|
|
*height = 0;
|
|
*numClipRects = 0;
|
|
*ppClipRects = NULL;
|
|
*backX = 0;
|
|
*backY = 0;
|
|
*numBackClipRects = 0;
|
|
*ppBackClipRects = NULL;
|
|
|
|
return GL_FALSE;
|
|
}
|
|
|
|
__glXDRIenterServer(GL_FALSE);
|
|
retval = DRIGetDrawableInfo(screenInfo.screens[screen],
|
|
pDrawable, index, stamp,
|
|
x, y, width, height,
|
|
numClipRects, &pClipRects,
|
|
backX, backY,
|
|
numBackClipRects, &pBackClipRects);
|
|
__glXDRIleaveServer(GL_FALSE);
|
|
|
|
if (*numClipRects > 0) {
|
|
size = sizeof (drm_clip_rect_t) * *numClipRects;
|
|
*ppClipRects = xalloc (size);
|
|
|
|
/* Clip cliprects to screen dimensions (redirected windows) */
|
|
if (*ppClipRects != NULL) {
|
|
ScreenPtr pScreen = screenInfo.screens[screen];
|
|
int i, j;
|
|
|
|
for (i = 0, j = 0; i < *numClipRects; i++) {
|
|
(*ppClipRects)[j].x1 = max(pClipRects[i].x1, 0);
|
|
(*ppClipRects)[j].y1 = max(pClipRects[i].y1, 0);
|
|
(*ppClipRects)[j].x2 = min(pClipRects[i].x2, pScreen->width);
|
|
(*ppClipRects)[j].y2 = min(pClipRects[i].y2, pScreen->height);
|
|
|
|
if ((*ppClipRects)[j].x1 < (*ppClipRects)[j].x2 &&
|
|
(*ppClipRects)[j].y1 < (*ppClipRects)[j].y2) {
|
|
j++;
|
|
}
|
|
}
|
|
|
|
if (*numClipRects != j) {
|
|
*numClipRects = j;
|
|
*ppClipRects = xrealloc (*ppClipRects,
|
|
sizeof (drm_clip_rect_t) *
|
|
*numClipRects);
|
|
}
|
|
} else
|
|
*numClipRects = 0;
|
|
}
|
|
else {
|
|
*ppClipRects = NULL;
|
|
}
|
|
|
|
if (*numBackClipRects > 0) {
|
|
size = sizeof (drm_clip_rect_t) * *numBackClipRects;
|
|
*ppBackClipRects = xalloc (size);
|
|
if (*ppBackClipRects != NULL)
|
|
memcpy (*ppBackClipRects, pBackClipRects, size);
|
|
}
|
|
else {
|
|
*ppBackClipRects = NULL;
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
static int
|
|
getUST(int64_t *ust)
|
|
{
|
|
struct timeval tv;
|
|
|
|
if (ust == NULL)
|
|
return -EFAULT;
|
|
|
|
if (gettimeofday(&tv, NULL) == 0) {
|
|
ust[0] = (tv.tv_sec * 1000000) + tv.tv_usec;
|
|
return 0;
|
|
} else {
|
|
return -errno;
|
|
}
|
|
}
|
|
|
|
/* Table of functions that we export to the driver. */
|
|
static const __DRIinterfaceMethods interface_methods = {
|
|
getProcAddress,
|
|
|
|
_gl_context_modes_create,
|
|
_gl_context_modes_destroy,
|
|
|
|
findScreen,
|
|
windowExists,
|
|
|
|
createContext,
|
|
destroyContext,
|
|
|
|
createDrawable,
|
|
destroyDrawable,
|
|
getDrawableInfo,
|
|
|
|
getUST,
|
|
NULL, /* glXGetMscRateOML, */
|
|
};
|
|
|
|
static const char dri_driver_path[] = DRI_DRIVER_PATH;
|
|
|
|
static Bool
|
|
glxDRIEnterVT (int index, int flags)
|
|
{
|
|
__GLXDRIscreen *screen = (__GLXDRIscreen *) __glXgetActiveScreen(index);
|
|
|
|
LogMessage(X_INFO, "AIGLX: Resuming AIGLX clients after VT switch\n");
|
|
|
|
if (!(*screen->enterVT) (index, flags))
|
|
return FALSE;
|
|
|
|
glxResumeClients();
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
glxDRILeaveVT (int index, int flags)
|
|
{
|
|
__GLXDRIscreen *screen = (__GLXDRIscreen *) __glXgetActiveScreen(index);
|
|
|
|
LogMessage(X_INFO, "AIGLX: Suspending AIGLX clients for VT switch\n");
|
|
|
|
glxSuspendClients();
|
|
|
|
return (*screen->leaveVT) (index, flags);
|
|
}
|
|
|
|
static __GLXscreen *
|
|
__glXDRIscreenProbe(ScreenPtr pScreen)
|
|
{
|
|
PFNCREATENEWSCREENFUNC createNewScreen;
|
|
drm_handle_t hSAREA;
|
|
drmAddress pSAREA = NULL;
|
|
char *BusID;
|
|
__DRIversion ddx_version;
|
|
__DRIversion dri_version;
|
|
__DRIversion drm_version;
|
|
__DRIframebuffer framebuffer;
|
|
int fd = -1;
|
|
int status;
|
|
int api_ver = 20070121;
|
|
drm_magic_t magic;
|
|
drmVersionPtr version;
|
|
int newlyopened;
|
|
char *driverName;
|
|
drm_handle_t hFB;
|
|
int junk;
|
|
__GLcontextModes * driver_modes;
|
|
__GLXDRIscreen *screen;
|
|
void *dev_priv = NULL;
|
|
char filename[128];
|
|
Bool isCapable;
|
|
size_t buffer_size;
|
|
ScrnInfoPtr pScrn = xf86Screens[pScreen->myNum];
|
|
|
|
if (!xf86LoaderCheckSymbol("DRIQueryDirectRenderingCapable") ||
|
|
!DRIQueryDirectRenderingCapable(pScreen, &isCapable) ||
|
|
!isCapable) {
|
|
LogMessage(X_INFO,
|
|
"AIGLX: Screen %d is not DRI capable\n", pScreen->myNum);
|
|
return NULL;
|
|
}
|
|
|
|
screen = xalloc(sizeof *screen);
|
|
if (screen == NULL)
|
|
return NULL;
|
|
memset(screen, 0, sizeof *screen);
|
|
|
|
screen->base.destroy = __glXDRIscreenDestroy;
|
|
screen->base.createContext = __glXDRIscreenCreateContext;
|
|
screen->base.createDrawable = __glXDRIscreenCreateDrawable;
|
|
screen->base.swapInterval = __glXDRIdrawableSwapInterval;
|
|
screen->base.pScreen = pScreen;
|
|
|
|
__glXInitExtensionEnableBits(screen->glx_enable_bits);
|
|
screen->driScreen.screenConfigs = screen;
|
|
|
|
|
|
/* DRI protocol version. */
|
|
dri_version.major = XF86DRI_MAJOR_VERSION;
|
|
dri_version.minor = XF86DRI_MINOR_VERSION;
|
|
dri_version.patch = XF86DRI_PATCH_VERSION;
|
|
|
|
framebuffer.base = NULL;
|
|
framebuffer.dev_priv = NULL;
|
|
|
|
if (!DRIOpenConnection(pScreen, &hSAREA, &BusID)) {
|
|
LogMessage(X_ERROR, "AIGLX error: DRIOpenConnection failed\n");
|
|
goto handle_error;
|
|
}
|
|
|
|
fd = drmOpenOnce(NULL, BusID, &newlyopened);
|
|
|
|
if (fd < 0) {
|
|
LogMessage(X_ERROR, "AIGLX error: drmOpenOnce failed (%s)\n",
|
|
strerror(-fd));
|
|
goto handle_error;
|
|
}
|
|
|
|
if (drmGetMagic(fd, &magic)) {
|
|
LogMessage(X_ERROR, "AIGLX error: drmGetMagic failed\n");
|
|
goto handle_error;
|
|
}
|
|
|
|
version = drmGetVersion(fd);
|
|
if (version) {
|
|
drm_version.major = version->version_major;
|
|
drm_version.minor = version->version_minor;
|
|
drm_version.patch = version->version_patchlevel;
|
|
drmFreeVersion(version);
|
|
}
|
|
else {
|
|
drm_version.major = -1;
|
|
drm_version.minor = -1;
|
|
drm_version.patch = -1;
|
|
}
|
|
|
|
if (newlyopened && !DRIAuthConnection(pScreen, magic)) {
|
|
LogMessage(X_ERROR, "AIGLX error: DRIAuthConnection failed\n");
|
|
goto handle_error;
|
|
}
|
|
|
|
/* Get device name (like "tdfx") and the ddx version numbers.
|
|
* We'll check the version in each DRI driver's "createNewScreen"
|
|
* function. */
|
|
if (!DRIGetClientDriverName(pScreen,
|
|
&ddx_version.major,
|
|
&ddx_version.minor,
|
|
&ddx_version.patch,
|
|
&driverName)) {
|
|
LogMessage(X_ERROR, "AIGLX error: DRIGetClientDriverName failed\n");
|
|
goto handle_error;
|
|
}
|
|
|
|
snprintf(filename, sizeof filename, "%s/%s_dri.so",
|
|
dri_driver_path, driverName);
|
|
|
|
screen->driver = dlopen(filename, RTLD_LAZY | RTLD_LOCAL);
|
|
if (screen->driver == NULL) {
|
|
LogMessage(X_ERROR, "AIGLX error: dlopen of %s failed (%s)\n",
|
|
filename, dlerror());
|
|
goto handle_error;
|
|
}
|
|
|
|
createNewScreen = dlsym(screen->driver, CREATE_NEW_SCREEN_FUNC);
|
|
if (createNewScreen == NULL) {
|
|
LogMessage(X_ERROR, "AIGLX error: dlsym for %s failed (%s)\n",
|
|
CREATE_NEW_SCREEN_FUNC, dlerror());
|
|
goto handle_error;
|
|
}
|
|
|
|
/*
|
|
* Get device-specific info. pDevPriv will point to a struct
|
|
* (such as DRIRADEONRec in xfree86/driver/ati/radeon_dri.h) that
|
|
* has information about the screen size, depth, pitch, ancilliary
|
|
* buffers, DRM mmap handles, etc.
|
|
*/
|
|
if (!DRIGetDeviceInfo(pScreen, &hFB, &junk,
|
|
&framebuffer.size, &framebuffer.stride,
|
|
&framebuffer.dev_priv_size, &framebuffer.dev_priv)) {
|
|
LogMessage(X_ERROR, "AIGLX error: XF86DRIGetDeviceInfo failed");
|
|
goto handle_error;
|
|
}
|
|
|
|
/* Sigh... the DRI interface is broken; the DRI driver will free
|
|
* the dev private pointer using _mesa_free() on screen destroy,
|
|
* but we can't use _mesa_malloc() here. In fact, the DRI driver
|
|
* shouldn't free data it didn't allocate itself, but what can you
|
|
* do... */
|
|
dev_priv = xalloc(framebuffer.dev_priv_size);
|
|
if (dev_priv == NULL) {
|
|
LogMessage(X_ERROR, "AIGLX error: dev_priv allocation failed");
|
|
goto handle_error;
|
|
}
|
|
memcpy(dev_priv, framebuffer.dev_priv, framebuffer.dev_priv_size);
|
|
framebuffer.dev_priv = dev_priv;
|
|
|
|
framebuffer.width = pScreen->width;
|
|
framebuffer.height = pScreen->height;
|
|
|
|
/* Map the framebuffer region. */
|
|
status = drmMap(fd, hFB, framebuffer.size,
|
|
(drmAddressPtr)&framebuffer.base);
|
|
if (status != 0) {
|
|
LogMessage(X_ERROR, "AIGLX error: drmMap of framebuffer failed (%s)",
|
|
strerror(-status));
|
|
goto handle_error;
|
|
}
|
|
|
|
/* Map the SAREA region. Further mmap regions may be setup in
|
|
* each DRI driver's "createNewScreen" function.
|
|
*/
|
|
status = drmMap(fd, hSAREA, SAREA_MAX, &pSAREA);
|
|
if (status != 0) {
|
|
LogMessage(X_ERROR, "AIGLX error: drmMap of SAREA failed (%s)",
|
|
strerror(-status));
|
|
goto handle_error;
|
|
}
|
|
|
|
driver_modes = NULL;
|
|
screen->driScreen.private =
|
|
(*createNewScreen)(NULL, pScreen->myNum,
|
|
&screen->driScreen,
|
|
screen->base.modes,
|
|
&ddx_version,
|
|
&dri_version,
|
|
&drm_version,
|
|
&framebuffer,
|
|
pSAREA,
|
|
fd,
|
|
api_ver,
|
|
&interface_methods,
|
|
&driver_modes);
|
|
|
|
if (screen->driScreen.private == NULL) {
|
|
LogMessage(X_ERROR, "AIGLX error: Calling driver entry point failed");
|
|
goto handle_error;
|
|
}
|
|
|
|
DRIGetTexOffsetFuncs(pScreen, &screen->texOffsetStart,
|
|
&screen->texOffsetFinish);
|
|
|
|
__glXScreenInit(&screen->base, pScreen);
|
|
|
|
buffer_size = __glXGetExtensionString(screen->glx_enable_bits, NULL);
|
|
if (buffer_size > 0) {
|
|
if (screen->base.GLXextensions != NULL) {
|
|
xfree(screen->base.GLXextensions);
|
|
}
|
|
|
|
screen->base.GLXextensions = xnfalloc(buffer_size);
|
|
(void) __glXGetExtensionString(screen->glx_enable_bits,
|
|
screen->base.GLXextensions);
|
|
}
|
|
|
|
|
|
filter_modes(&screen->base.modes, driver_modes);
|
|
_gl_context_modes_destroy(driver_modes);
|
|
|
|
__glXsetEnterLeaveServerFuncs(__glXDRIenterServer, __glXDRIleaveServer);
|
|
|
|
screen->enterVT = pScrn->EnterVT;
|
|
pScrn->EnterVT = glxDRIEnterVT;
|
|
screen->leaveVT = pScrn->LeaveVT;
|
|
pScrn->LeaveVT = glxDRILeaveVT;
|
|
|
|
LogMessage(X_INFO,
|
|
"AIGLX: Loaded and initialized %s\n", filename);
|
|
|
|
return &screen->base;
|
|
|
|
handle_error:
|
|
if (pSAREA != NULL)
|
|
drmUnmap(pSAREA, SAREA_MAX);
|
|
|
|
if (framebuffer.base != NULL)
|
|
drmUnmap((drmAddress)framebuffer.base, framebuffer.size);
|
|
|
|
if (dev_priv != NULL)
|
|
xfree(dev_priv);
|
|
|
|
if (fd >= 0)
|
|
drmCloseOnce(fd);
|
|
|
|
DRICloseConnection(pScreen);
|
|
|
|
if (screen->driver)
|
|
dlclose(screen->driver);
|
|
|
|
xfree(screen);
|
|
|
|
LogMessage(X_ERROR, "AIGLX: reverting to software rendering\n");
|
|
|
|
return NULL;
|
|
}
|
|
|
|
__GLXprovider __glXDRIProvider = {
|
|
__glXDRIscreenProbe,
|
|
"DRI",
|
|
NULL
|
|
};
|