483 lines
16 KiB
C
483 lines
16 KiB
C
|
/*
|
||
|
* Copyright (c) 2016, NVIDIA CORPORATION.
|
||
|
*
|
||
|
* Permission is hereby granted, free of charge, to any person obtaining a
|
||
|
* copy of this software and/or associated documentation files (the
|
||
|
* "Materials"), to deal in the Materials without restriction, including
|
||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||
|
* distribute, sublicense, and/or sell copies of the Materials, and to
|
||
|
* permit persons to whom the Materials are furnished to do so, subject to
|
||
|
* the following conditions:
|
||
|
*
|
||
|
* The above copyright notice and this permission notice shall be included
|
||
|
* unaltered in all copies or substantial portions of the Materials.
|
||
|
* Any additions, deletions, or changes to the original source files
|
||
|
* must be clearly indicated in accompanying documentation.
|
||
|
*
|
||
|
* If only executable code is distributed, then the accompanying
|
||
|
* documentation must state that "this software is based in part on the
|
||
|
* work of the Khronos Group."
|
||
|
*
|
||
|
* THE MATERIALS ARE 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 AUTHORS OR COPYRIGHT HOLDERS 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
|
||
|
* MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
|
||
|
*/
|
||
|
|
||
|
#include <dix-config.h>
|
||
|
|
||
|
#include "hashtable.h"
|
||
|
#include "vndserver.h"
|
||
|
#include "vndservervendor.h"
|
||
|
|
||
|
/**
|
||
|
* The length of the dispatchFuncs array. Every opcode above this is a
|
||
|
* X_GLsop_* code, which all can use the same handler.
|
||
|
*/
|
||
|
#define OPCODE_ARRAY_LEN 100
|
||
|
|
||
|
// This hashtable is used to keep track of the dispatch stubs for
|
||
|
// GLXVendorPrivate and GLXVendorPrivateWithReply.
|
||
|
typedef struct GlxVendorPrivDispatchRec {
|
||
|
CARD32 vendorCode;
|
||
|
GlxServerDispatchProc proc;
|
||
|
HashTable hh;
|
||
|
} GlxVendorPrivDispatch;
|
||
|
|
||
|
static GlxServerDispatchProc dispatchFuncs[OPCODE_ARRAY_LEN] = {};
|
||
|
static HashTable vendorPrivHash = NULL;
|
||
|
static HtGenericHashSetupRec vendorPrivSetup = {
|
||
|
.keySize = sizeof(CARD32)
|
||
|
};
|
||
|
|
||
|
static int DispatchBadRequest(ClientPtr client)
|
||
|
{
|
||
|
return BadRequest;
|
||
|
}
|
||
|
|
||
|
static GlxVendorPrivDispatch *LookupVendorPrivDispatch(CARD32 vendorCode, Bool create)
|
||
|
{
|
||
|
GlxVendorPrivDispatch *disp = NULL;
|
||
|
|
||
|
disp = ht_find(vendorPrivHash, &vendorCode);
|
||
|
if (disp == NULL && create) {
|
||
|
if ((disp = ht_add(vendorPrivHash, &vendorCode))) {
|
||
|
disp->vendorCode = vendorCode;
|
||
|
disp->proc = NULL;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return disp;
|
||
|
}
|
||
|
|
||
|
static GlxServerDispatchProc GetVendorDispatchFunc(CARD8 opcode, CARD32 vendorCode)
|
||
|
{
|
||
|
GlxServerVendor *vendor;
|
||
|
|
||
|
xorg_list_for_each_entry(vendor, &GlxVendorList, entry) {
|
||
|
GlxServerDispatchProc proc = vendor->glxvc.getDispatchAddress(opcode, vendorCode);
|
||
|
if (proc != NULL) {
|
||
|
return proc;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return DispatchBadRequest;
|
||
|
}
|
||
|
|
||
|
static void SetReplyHeader(ClientPtr client, void *replyPtr)
|
||
|
{
|
||
|
xGenericReply *rep = (xGenericReply *) replyPtr;
|
||
|
rep->type = X_Reply;
|
||
|
rep->sequenceNumber = client->sequence;
|
||
|
rep->length = 0;
|
||
|
}
|
||
|
|
||
|
/* Include the trivial dispatch handlers */
|
||
|
#include "vnd_dispatch_stubs.c"
|
||
|
|
||
|
static int dispatch_GLXQueryVersion(ClientPtr client)
|
||
|
{
|
||
|
xGLXQueryVersionReply reply;
|
||
|
REQUEST_SIZE_MATCH(xGLXQueryVersionReq);
|
||
|
|
||
|
SetReplyHeader(client, &reply);
|
||
|
reply.majorVersion = GlxCheckSwap(client, 1);
|
||
|
reply.minorVersion = GlxCheckSwap(client, 4);
|
||
|
|
||
|
WriteToClient(client, sz_xGLXQueryVersionReply, &reply);
|
||
|
return Success;
|
||
|
}
|
||
|
|
||
|
/* broken header workaround */
|
||
|
#ifndef X_GLXSetClientInfo2ARB
|
||
|
#define X_GLXSetClientInfo2ARB X_GLXSetConfigInfo2ARB
|
||
|
#endif
|
||
|
|
||
|
/**
|
||
|
* This function is used for X_GLXClientInfo, X_GLXSetClientInfoARB, and
|
||
|
* X_GLXSetClientInfo2ARB.
|
||
|
*/
|
||
|
static int dispatch_GLXClientInfo(ClientPtr client)
|
||
|
{
|
||
|
GlxServerVendor *vendor;
|
||
|
void *requestCopy = NULL;
|
||
|
size_t requestSize = client->req_len * 4;
|
||
|
|
||
|
if (client->minorOp == X_GLXClientInfo) {
|
||
|
REQUEST_AT_LEAST_SIZE(xGLXClientInfoReq);
|
||
|
} else if (client->minorOp == X_GLXSetClientInfoARB) {
|
||
|
REQUEST_AT_LEAST_SIZE(xGLXSetClientInfoARBReq);
|
||
|
} else if (client->minorOp == X_GLXSetClientInfo2ARB) {
|
||
|
REQUEST_AT_LEAST_SIZE(xGLXSetClientInfo2ARBReq);
|
||
|
} else {
|
||
|
return BadImplementation;
|
||
|
}
|
||
|
|
||
|
// We'll forward this request to each vendor library. Since a vendor might
|
||
|
// modify the request data in place (e.g., for byte swapping), make a copy
|
||
|
// of the request first.
|
||
|
requestCopy = malloc(requestSize);
|
||
|
if (requestCopy == NULL) {
|
||
|
return BadAlloc;
|
||
|
}
|
||
|
memcpy(requestCopy, client->requestBuffer, requestSize);
|
||
|
|
||
|
xorg_list_for_each_entry(vendor, &GlxVendorList, entry) {
|
||
|
vendor->glxvc.handleRequest(client);
|
||
|
// Revert the request buffer back to our copy.
|
||
|
memcpy(client->requestBuffer, requestCopy, requestSize);
|
||
|
}
|
||
|
free(requestCopy);
|
||
|
return Success;
|
||
|
}
|
||
|
|
||
|
static int CommonLoseCurrent(ClientPtr client, GlxContextTagInfo *tagInfo)
|
||
|
{
|
||
|
int ret;
|
||
|
|
||
|
ret = tagInfo->vendor->glxvc.makeCurrent(client,
|
||
|
tagInfo->tag, // No old context tag,
|
||
|
None, None, None, 0);
|
||
|
|
||
|
if (ret == Success) {
|
||
|
GlxFreeContextTag(tagInfo);
|
||
|
}
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static int CommonMakeNewCurrent(ClientPtr client,
|
||
|
GlxServerVendor *vendor,
|
||
|
GLXDrawable drawable,
|
||
|
GLXDrawable readdrawable,
|
||
|
GLXContextID context,
|
||
|
GLXContextTag *newContextTag)
|
||
|
{
|
||
|
int ret = BadAlloc;
|
||
|
GlxContextTagInfo *tagInfo;
|
||
|
|
||
|
tagInfo = GlxAllocContextTag(client, vendor);
|
||
|
|
||
|
if (tagInfo) {
|
||
|
ret = vendor->glxvc.makeCurrent(client,
|
||
|
0, // No old context tag,
|
||
|
drawable, readdrawable, context,
|
||
|
tagInfo->tag);
|
||
|
|
||
|
if (ret == Success) {
|
||
|
tagInfo->drawable = drawable;
|
||
|
tagInfo->readdrawable = readdrawable;
|
||
|
tagInfo->context = context;
|
||
|
*newContextTag = tagInfo->tag;
|
||
|
} else {
|
||
|
GlxFreeContextTag(tagInfo);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static int CommonMakeCurrent(ClientPtr client,
|
||
|
GLXContextTag oldContextTag,
|
||
|
GLXDrawable drawable,
|
||
|
GLXDrawable readdrawable,
|
||
|
GLXContextID context)
|
||
|
{
|
||
|
xGLXMakeCurrentReply reply = {};
|
||
|
GlxContextTagInfo *oldTag = NULL;
|
||
|
GlxServerVendor *newVendor = NULL;
|
||
|
|
||
|
oldContextTag = GlxCheckSwap(client, oldContextTag);
|
||
|
drawable = GlxCheckSwap(client, drawable);
|
||
|
readdrawable = GlxCheckSwap(client, readdrawable);
|
||
|
context = GlxCheckSwap(client, context);
|
||
|
|
||
|
SetReplyHeader(client, &reply);
|
||
|
|
||
|
if (oldContextTag != 0) {
|
||
|
oldTag = GlxLookupContextTag(client, oldContextTag);
|
||
|
if (oldTag == NULL) {
|
||
|
return GlxErrorBase + GLXBadContextTag;
|
||
|
}
|
||
|
}
|
||
|
if (context != 0) {
|
||
|
newVendor = GlxGetXIDMap(context);
|
||
|
if (newVendor == NULL) {
|
||
|
return GlxErrorBase + GLXBadContext;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (oldTag == NULL && newVendor == NULL) {
|
||
|
// Nothing to do here. Just send a successful reply.
|
||
|
reply.contextTag = 0;
|
||
|
} else if (oldTag != NULL && newVendor != NULL
|
||
|
&& oldTag->context == context
|
||
|
&& oldTag->drawable == drawable
|
||
|
&& oldTag->readdrawable == readdrawable)
|
||
|
{
|
||
|
// The old and new values are all the same, so send a successful reply.
|
||
|
reply.contextTag = oldTag->tag;
|
||
|
} else {
|
||
|
// TODO: For switching contexts in a single vendor, just make one
|
||
|
// makeCurrent call?
|
||
|
|
||
|
// TODO: When changing vendors, would it be better to do the
|
||
|
// MakeCurrent(new) first, then the LoseCurrent(old)?
|
||
|
// If the MakeCurrent(new) fails, then the old context will still be current.
|
||
|
// If the LoseCurrent(old) fails, then we can (probably) undo the MakeCurrent(new) with
|
||
|
// a LoseCurrent(old).
|
||
|
// But, if the recovery LoseCurrent(old) fails, then we're really in a bad state.
|
||
|
|
||
|
// Clear the old context first.
|
||
|
if (oldTag != NULL) {
|
||
|
int ret = CommonLoseCurrent(client, oldTag);
|
||
|
if (ret != Success) {
|
||
|
return ret;
|
||
|
}
|
||
|
oldTag = NULL;
|
||
|
}
|
||
|
|
||
|
if (newVendor != NULL) {
|
||
|
int ret = CommonMakeNewCurrent(client, newVendor, drawable, readdrawable, context, &reply.contextTag);
|
||
|
if (ret != Success) {
|
||
|
return ret;
|
||
|
}
|
||
|
} else {
|
||
|
reply.contextTag = 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
reply.contextTag = GlxCheckSwap(client, reply.contextTag);
|
||
|
WriteToClient(client, sz_xGLXMakeCurrentReply, &reply);
|
||
|
return Success;
|
||
|
}
|
||
|
|
||
|
static int dispatch_GLXMakeCurrent(ClientPtr client)
|
||
|
{
|
||
|
REQUEST(xGLXMakeCurrentReq);
|
||
|
REQUEST_SIZE_MATCH(*stuff);
|
||
|
|
||
|
return CommonMakeCurrent(client, stuff->oldContextTag,
|
||
|
stuff->drawable, stuff->drawable, stuff->context);
|
||
|
}
|
||
|
|
||
|
static int dispatch_GLXMakeContextCurrent(ClientPtr client)
|
||
|
{
|
||
|
REQUEST(xGLXMakeContextCurrentReq);
|
||
|
REQUEST_SIZE_MATCH(*stuff);
|
||
|
|
||
|
return CommonMakeCurrent(client, stuff->oldContextTag,
|
||
|
stuff->drawable, stuff->readdrawable, stuff->context);
|
||
|
}
|
||
|
|
||
|
static int dispatch_GLXMakeCurrentReadSGI(ClientPtr client)
|
||
|
{
|
||
|
REQUEST(xGLXMakeCurrentReadSGIReq);
|
||
|
REQUEST_SIZE_MATCH(*stuff);
|
||
|
|
||
|
return CommonMakeCurrent(client, stuff->oldContextTag,
|
||
|
stuff->drawable, stuff->readable, stuff->context);
|
||
|
}
|
||
|
|
||
|
static int dispatch_GLXCopyContext(ClientPtr client)
|
||
|
{
|
||
|
REQUEST(xGLXCopyContextReq);
|
||
|
GlxServerVendor *vendor;
|
||
|
REQUEST_SIZE_MATCH(*stuff);
|
||
|
|
||
|
// If we've got a context tag, then we'll use it to select a vendor. If we
|
||
|
// don't have a tag, then we'll look up one of the contexts. In either
|
||
|
// case, it's up to the vendor library to make sure that the context ID's
|
||
|
// are valid.
|
||
|
if (stuff->contextTag != 0) {
|
||
|
GlxContextTagInfo *tagInfo = GlxLookupContextTag(client, GlxCheckSwap(client, stuff->contextTag));
|
||
|
if (tagInfo == NULL) {
|
||
|
return GlxErrorBase + GLXBadContextTag;
|
||
|
}
|
||
|
vendor = tagInfo->vendor;
|
||
|
} else {
|
||
|
vendor = GlxGetXIDMap(GlxCheckSwap(client, stuff->source));
|
||
|
if (vendor == NULL) {
|
||
|
return GlxErrorBase + GLXBadContext;
|
||
|
}
|
||
|
}
|
||
|
return vendor->glxvc.handleRequest(client);
|
||
|
}
|
||
|
|
||
|
static int dispatch_GLXSwapBuffers(ClientPtr client)
|
||
|
{
|
||
|
GlxServerVendor *vendor = NULL;
|
||
|
REQUEST(xGLXSwapBuffersReq);
|
||
|
REQUEST_SIZE_MATCH(*stuff);
|
||
|
|
||
|
if (stuff->contextTag != 0) {
|
||
|
// If the request has a context tag, then look up a vendor from that.
|
||
|
// The vendor library is then responsible for validating the drawable.
|
||
|
GlxContextTagInfo *tagInfo = GlxLookupContextTag(client, GlxCheckSwap(client, stuff->contextTag));
|
||
|
if (tagInfo == NULL) {
|
||
|
return GlxErrorBase + GLXBadContextTag;
|
||
|
}
|
||
|
vendor = tagInfo->vendor;
|
||
|
} else {
|
||
|
// We don't have a context tag, so look up the vendor from the
|
||
|
// drawable.
|
||
|
vendor = GlxGetXIDMap(GlxCheckSwap(client, stuff->drawable));
|
||
|
if (vendor == NULL) {
|
||
|
return GlxErrorBase + GLXBadDrawable;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return vendor->glxvc.handleRequest(client);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This is a generic handler for all of the X_GLXsop* requests.
|
||
|
*/
|
||
|
static int dispatch_GLXSingle(ClientPtr client)
|
||
|
{
|
||
|
REQUEST(xGLXSingleReq);
|
||
|
GlxContextTagInfo *tagInfo;
|
||
|
REQUEST_AT_LEAST_SIZE(*stuff);
|
||
|
|
||
|
tagInfo = GlxLookupContextTag(client, GlxCheckSwap(client, stuff->contextTag));
|
||
|
if (tagInfo != NULL) {
|
||
|
return tagInfo->vendor->glxvc.handleRequest(client);
|
||
|
} else {
|
||
|
return GlxErrorBase + GLXBadContextTag;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static int dispatch_GLXVendorPriv(ClientPtr client)
|
||
|
{
|
||
|
GlxVendorPrivDispatch *disp;
|
||
|
REQUEST(xGLXVendorPrivateReq);
|
||
|
REQUEST_AT_LEAST_SIZE(*stuff);
|
||
|
|
||
|
disp = LookupVendorPrivDispatch(GlxCheckSwap(client, stuff->vendorCode), TRUE);
|
||
|
if (disp == NULL) {
|
||
|
return BadAlloc;
|
||
|
}
|
||
|
|
||
|
if (disp->proc == NULL) {
|
||
|
// We don't have a dispatch function for this request yet. Check with
|
||
|
// each vendor library to find one.
|
||
|
// Note that even if none of the vendors provides a dispatch stub,
|
||
|
// we'll still add an entry to the dispatch table, so that we don't
|
||
|
// have to look it up again later.
|
||
|
|
||
|
disp->proc = GetVendorDispatchFunc(stuff->glxCode,
|
||
|
GlxCheckSwap(client,
|
||
|
stuff->vendorCode));
|
||
|
}
|
||
|
return disp->proc(client);
|
||
|
}
|
||
|
|
||
|
Bool GlxDispatchInit(void)
|
||
|
{
|
||
|
GlxVendorPrivDispatch *disp;
|
||
|
|
||
|
vendorPrivHash = ht_create(sizeof(CARD32), sizeof(GlxVendorPrivDispatch),
|
||
|
ht_generic_hash, ht_generic_compare,
|
||
|
(void *) &vendorPrivSetup);
|
||
|
if (!vendorPrivHash) {
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
// Assign a custom dispatch stub GLXMakeCurrentReadSGI. This is the only
|
||
|
// vendor private request that we need to deal with in libglvnd itself.
|
||
|
disp = LookupVendorPrivDispatch(X_GLXvop_MakeCurrentReadSGI, TRUE);
|
||
|
if (disp == NULL) {
|
||
|
return FALSE;
|
||
|
}
|
||
|
disp->proc = dispatch_GLXMakeCurrentReadSGI;
|
||
|
|
||
|
// Assign the dispatch stubs for requests that need special handling.
|
||
|
dispatchFuncs[X_GLXQueryVersion] = dispatch_GLXQueryVersion;
|
||
|
dispatchFuncs[X_GLXMakeCurrent] = dispatch_GLXMakeCurrent;
|
||
|
dispatchFuncs[X_GLXMakeContextCurrent] = dispatch_GLXMakeContextCurrent;
|
||
|
dispatchFuncs[X_GLXCopyContext] = dispatch_GLXCopyContext;
|
||
|
dispatchFuncs[X_GLXSwapBuffers] = dispatch_GLXSwapBuffers;
|
||
|
|
||
|
dispatchFuncs[X_GLXClientInfo] = dispatch_GLXClientInfo;
|
||
|
dispatchFuncs[X_GLXSetClientInfoARB] = dispatch_GLXClientInfo;
|
||
|
dispatchFuncs[X_GLXSetClientInfo2ARB] = dispatch_GLXClientInfo;
|
||
|
|
||
|
dispatchFuncs[X_GLXVendorPrivate] = dispatch_GLXVendorPriv;
|
||
|
dispatchFuncs[X_GLXVendorPrivateWithReply] = dispatch_GLXVendorPriv;
|
||
|
|
||
|
// Assign the trivial stubs
|
||
|
dispatchFuncs[X_GLXRender] = dispatch_Render;
|
||
|
dispatchFuncs[X_GLXRenderLarge] = dispatch_RenderLarge;
|
||
|
dispatchFuncs[X_GLXCreateContext] = dispatch_CreateContext;
|
||
|
dispatchFuncs[X_GLXDestroyContext] = dispatch_DestroyContext;
|
||
|
dispatchFuncs[X_GLXWaitGL] = dispatch_WaitGL;
|
||
|
dispatchFuncs[X_GLXWaitX] = dispatch_WaitX;
|
||
|
dispatchFuncs[X_GLXUseXFont] = dispatch_UseXFont;
|
||
|
dispatchFuncs[X_GLXCreateGLXPixmap] = dispatch_CreateGLXPixmap;
|
||
|
dispatchFuncs[X_GLXGetVisualConfigs] = dispatch_GetVisualConfigs;
|
||
|
dispatchFuncs[X_GLXDestroyGLXPixmap] = dispatch_DestroyGLXPixmap;
|
||
|
dispatchFuncs[X_GLXQueryExtensionsString] = dispatch_QueryExtensionsString;
|
||
|
dispatchFuncs[X_GLXQueryServerString] = dispatch_QueryServerString;
|
||
|
dispatchFuncs[X_GLXChangeDrawableAttributes] = dispatch_ChangeDrawableAttributes;
|
||
|
dispatchFuncs[X_GLXCreateNewContext] = dispatch_CreateNewContext;
|
||
|
dispatchFuncs[X_GLXCreatePbuffer] = dispatch_CreatePbuffer;
|
||
|
dispatchFuncs[X_GLXCreatePixmap] = dispatch_CreatePixmap;
|
||
|
dispatchFuncs[X_GLXCreateWindow] = dispatch_CreateWindow;
|
||
|
dispatchFuncs[X_GLXCreateContextAttribsARB] = dispatch_CreateContextAttribsARB;
|
||
|
dispatchFuncs[X_GLXDestroyPbuffer] = dispatch_DestroyPbuffer;
|
||
|
dispatchFuncs[X_GLXDestroyPixmap] = dispatch_DestroyPixmap;
|
||
|
dispatchFuncs[X_GLXDestroyWindow] = dispatch_DestroyWindow;
|
||
|
dispatchFuncs[X_GLXGetDrawableAttributes] = dispatch_GetDrawableAttributes;
|
||
|
dispatchFuncs[X_GLXGetFBConfigs] = dispatch_GetFBConfigs;
|
||
|
dispatchFuncs[X_GLXQueryContext] = dispatch_QueryContext;
|
||
|
dispatchFuncs[X_GLXIsDirect] = dispatch_IsDirect;
|
||
|
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
void GlxDispatchReset(void)
|
||
|
{
|
||
|
memset(dispatchFuncs, 0, sizeof(dispatchFuncs));
|
||
|
|
||
|
ht_destroy(vendorPrivHash);
|
||
|
vendorPrivHash = NULL;
|
||
|
}
|
||
|
|
||
|
int GlxDispatchRequest(ClientPtr client)
|
||
|
{
|
||
|
REQUEST(xReq);
|
||
|
if (GlxExtensionEntry->base == 0)
|
||
|
return BadRequest;
|
||
|
if (stuff->data < OPCODE_ARRAY_LEN) {
|
||
|
if (dispatchFuncs[stuff->data] == NULL) {
|
||
|
// Try to find a dispatch stub.
|
||
|
dispatchFuncs[stuff->data] = GetVendorDispatchFunc(stuff->data, 0);
|
||
|
}
|
||
|
return dispatchFuncs[stuff->data](client);
|
||
|
} else {
|
||
|
return dispatch_GLXSingle(client);
|
||
|
}
|
||
|
}
|