Sweden-Number/dlls/wined3d/swapchain.c

478 lines
19 KiB
C
Raw Normal View History

2005-06-23 13:05:24 +02:00
/*
*IDirect3DSwapChain9 implementation
*
*Copyright 2002-2003 Jason Edmeades
*Copyright 2002-2003 Raphael Junqueira
*Copyright 2005 Oliver Stieber
*
*This library 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.
*
*This library 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 this library; if not, write to the Free Software
*Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "config.h"
#include "wined3d_private.h"
/* TODO: move to shared header (or context manager )*/
/* x11drv GDI escapes */
#define X11DRV_ESCAPE 6789
enum x11drv_escape_codes
{
X11DRV_GET_DISPLAY, /* get X11 display for a DC */
X11DRV_GET_DRAWABLE, /* get current drawable for a DC */
X11DRV_GET_FONT, /* get current X font for a DC */
};
/* retrieve the X display to use on a given DC */
inline static Display *get_display( HDC hdc )
{
Display *display;
enum x11drv_escape_codes escape = X11DRV_GET_DISPLAY;
if (!ExtEscape( hdc, X11DRV_ESCAPE, sizeof(escape), (LPCSTR)&escape,
sizeof(display), (LPSTR)&display )) display = NULL;
return display;
}
2005-07-13 16:15:54 +02:00
/*TODO: some of the additional parameters may be required to
2005-06-23 13:05:24 +02:00
set the gamma ramp (for some weird reason microsoft have left swap gammaramp in device
but it operates on a swapchain, it may be a good idea to move it to IWineD3DSwapChain for IWineD3D)*/
2005-07-13 16:15:54 +02:00
2005-06-23 13:05:24 +02:00
WINE_DEFAULT_DEBUG_CHANNEL(d3d);
WINE_DECLARE_DEBUG_CHANNEL(d3d_fps);
/* IDirect3DSwapChain IUnknown parts follow: */
ULONG WINAPI IWineD3DSwapChainImpl_AddRef(IWineD3DSwapChain *iface) {
IWineD3DSwapChainImpl *This = (IWineD3DSwapChainImpl *)iface;
DWORD refCount = InterlockedIncrement(&This->ref);
TRACE("(%p) : AddRef increasing from %ld\n", This, refCount - 1);
return refCount;
}
HRESULT WINAPI IWineD3DSwapChainImpl_QueryInterface(IWineD3DSwapChain *iface, REFIID riid, LPVOID *ppobj)
{
IWineD3DSwapChainImpl *This = (IWineD3DSwapChainImpl *)iface;
TRACE("(%p)->(%s,%p) \n",This,debugstr_guid(riid),ppobj);
if (IsEqualGUID(riid, &IID_IUnknown)
|| IsEqualGUID(riid, &IID_IWineD3DSwapChain)){
IWineD3DSwapChainImpl_AddRef(iface);
if(ppobj == NULL){
ERR("Query interface called but now data allocated\n");
return E_NOINTERFACE;
}
*ppobj = This;
return D3D_OK;
2005-07-13 16:15:54 +02:00
}
2005-06-23 13:05:24 +02:00
return E_NOINTERFACE;
}
ULONG WINAPI IWineD3DSwapChainImpl_Release(IWineD3DSwapChain *iface) {
IWineD3DSwapChainImpl *This = (IWineD3DSwapChainImpl *)iface;
2005-07-13 16:15:54 +02:00
DWORD refCount;
2005-06-23 13:05:24 +02:00
refCount = InterlockedDecrement(&This->ref);
TRACE("(%p) : ReleaseRef to %ld\n", This, refCount);
if (refCount == 0) {
IUnknown* bufferParent;
/* tell the device that we've been released */
IWineD3DDevice_SwapChainReleased((IWineD3DDevice *)This->wineD3DDevice, iface);
/* release the ref to the front and back buffer parents */
2005-06-23 13:05:24 +02:00
IWineD3DSurface_GetParent(This->frontBuffer, &bufferParent);
IUnknown_Release(bufferParent); /* once for the get parent */
if(IUnknown_Release(bufferParent) > 0){
FIXME("(%p) Something's still holding the front buffer\n",This);
}
IWineD3DSurface_GetParent(This->backBuffer, &bufferParent);
IUnknown_Release(bufferParent); /* once for the get parent */
if(IUnknown_Release(bufferParent) > 0){
FIXME("(%p) Something's still holding the back buffer\n",This);
}
/* Clean up the context */
/* check that we are the current context first */
if(glXGetCurrentContext() == This->glCtx){
glXMakeCurrent(This->display, None, NULL);
}
glXDestroyContext(This->display, This->glCtx);
/* IUnknown_Release(This->parent); This should only apply to the primary swapchain,
all others are crated by the caller, so releasing the parent should cause
the child to be released, not the other way around!
*/
HeapFree(GetProcessHeap(), 0, This);
}
return refCount;
}
HRESULT WINAPI IWineD3DSwapChainImpl_GetParent(IWineD3DSwapChain *iface, IUnknown ** ppParent){
IWineD3DSwapChainImpl *This = (IWineD3DSwapChainImpl *)iface;
*ppParent = This->parent;
IUnknown_AddRef(*ppParent);
TRACE("(%p) returning %p\n", This , *ppParent);
return D3D_OK;
}
/*IWineD3DSwapChain parts follow: */
HRESULT WINAPI IWineD3DSwapChainImpl_Present(IWineD3DSwapChain *iface, CONST RECT *pSourceRect, CONST RECT *pDestRect, HWND hDestWindowOverride, CONST RGNDATA *pDirtyRegion, DWORD dwFlags) {
IWineD3DSwapChainImpl *This = (IWineD3DSwapChainImpl *)iface;
2005-07-13 16:15:54 +02:00
2005-06-23 13:05:24 +02:00
ENTER_GL();
if (pSourceRect || pDestRect) FIXME("Unhandled present options %p/%p\n", pSourceRect, pDestRect);
/* TODO: If only source rect or dest rect are supplied then clip the window to match */
TRACE("preseting display %p, drawable %ld\n", This->display, This->drawable);
2005-07-13 16:15:54 +02:00
2005-06-23 13:05:24 +02:00
/* Don't call checkGLcall, as glGetError is not applicable here */
if (hDestWindowOverride && This->win_handle != hDestWindowOverride) {
/* Set this swapchain up to point to the new destination.. */
#ifdef USE_CONTEXT_MANAGER
/* TODO: use a context mamager */
#endif
2005-07-13 16:15:54 +02:00
2005-06-23 13:05:24 +02:00
/* FIXME: Never access */
IWineD3DSwapChainImpl *swapChainImpl;
IWineD3DDevice_GetSwapChain((IWineD3DDevice *)This->wineD3DDevice, 0 , (IWineD3DSwapChain **)&swapChainImpl);
FIXME("Unable to render to a destination window %p\n", hDestWindowOverride );
2005-06-23 13:05:24 +02:00
if(This == swapChainImpl){
/* FIXME: this will be fixed by moving to a context management system */
2005-07-11 12:59:41 +02:00
FIXME("Cannot change the target of the implicit swapchain\n");
2005-06-23 13:05:24 +02:00
}else{
HDC hDc;
XVisualInfo template;
int num;
Display *oldDisplay = This->display;
GLXContext oldContext = This->glCtx;
2005-07-13 16:15:54 +02:00
IUnknown* tmp;
2005-06-23 13:05:24 +02:00
GLXContext currentContext;
Drawable currentDrawable;
hDc = GetDC(hDestWindowOverride);
2005-07-13 16:15:54 +02:00
This->win_handle = hDestWindowOverride;
2005-06-23 13:05:24 +02:00
This->win = (Window)GetPropA( hDestWindowOverride, "__wine_x11_whole_window" );
TRACE("Creating a new context for the window %p \n", hDestWindowOverride);
ENTER_GL();
TRACE("Desctroying context %p %p\n", This->display, This->render_ctx);
LEAVE_GL();
ENTER_GL();
This->display = get_display(hDc);
TRACE("Got display%p for %p %p\n", This->display, hDc, hDestWindowOverride);
ReleaseDC(hDestWindowOverride, hDc);
template.visualid = (VisualID)GetPropA(GetDesktopWindow(), "__wine_x11_visual_id");
This->visInfo = XGetVisualInfo(This->display, VisualIDMask, &template, &num);
if (NULL == This->visInfo) {
2005-07-13 16:15:54 +02:00
ERR("cannot really get XVisual\n");
2005-06-23 13:05:24 +02:00
LEAVE_GL();
return D3DERR_NOTAVAILABLE;
}
2005-07-11 12:59:41 +02:00
/* Now we have problems? well not really we just need to know what the implicit context is */
2005-06-23 13:05:24 +02:00
/* now destroy the old context and create a new one (we should really copy the buffers over, and do the whole make current thing! */
/* destroy the active context?*/
TRACE("Creating new context for %p %p %p\n",This->display, This->visInfo, swapChainImpl->glCtx);
This->glCtx = glXCreateContext(This->display, This->visInfo, swapChainImpl->glCtx, GL_TRUE);
if (NULL == This->glCtx) {
2005-07-13 16:15:54 +02:00
ERR("cannot create glxContext\n");
2005-06-23 13:05:24 +02:00
}
This->drawable = This->win;
This->render_ctx = This->glCtx;
2005-07-11 12:59:41 +02:00
/* Setup some default states TODO: apply the stateblock to the new context */
2005-06-23 13:05:24 +02:00
/** save current context and drawable **/
currentContext = glXGetCurrentContext();
currentDrawable = glXGetCurrentDrawable();
if (glXMakeCurrent(This->display, This->win, This->glCtx) == False) {
ERR("Error in setting current context (display %p context %p drawable %ld)!\n", This->display, This->glCtx, This->win);
}
checkGLcall("glXMakeCurrent");
/* Clear the screen */
glClearColor(0.0, 0.0, 0.0, 0.0);
checkGLcall("glClearColor");
glClearIndex(0);
glClearDepth(1);
glClearStencil(0);
2005-07-13 16:15:54 +02:00
2005-06-23 13:05:24 +02:00
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ACCUM_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
checkGLcall("glClear");
2005-07-13 16:15:54 +02:00
2005-06-23 13:05:24 +02:00
glColor3f(1.0, 1.0, 1.0);
checkGLcall("glColor3f");
2005-07-13 16:15:54 +02:00
2005-06-23 13:05:24 +02:00
glEnable(GL_LIGHTING);
checkGLcall("glEnable");
2005-07-13 16:15:54 +02:00
2005-06-23 13:05:24 +02:00
glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE);
checkGLcall("glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE);");
2005-07-13 16:15:54 +02:00
2005-06-23 13:05:24 +02:00
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_EXT);
checkGLcall("glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_EXT);");
2005-07-13 16:15:54 +02:00
2005-06-23 13:05:24 +02:00
glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL, GL_SEPARATE_SPECULAR_COLOR);
checkGLcall("glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL, GL_SEPARATE_SPECULAR_COLOR);");
2005-07-13 16:15:54 +02:00
2005-06-23 13:05:24 +02:00
/* If this swapchain is currently the active context then make this swapchain active */
if(IWineD3DSurface_GetContainer(This->wineD3DDevice->renderTarget, &IID_IWineD3DSwapChain, (void **)&tmp) == D3D_OK){
2005-06-23 13:05:24 +02:00
if(tmp != (IUnknown *)This){
glXMakeCurrent(This->display, currentDrawable, currentContext);
checkGLcall("glXMakeCurrent");
}
IUnknown_Release(tmp);
}else{
/* reset the context */
glXMakeCurrent(This->display, currentDrawable, currentContext);
2005-07-13 16:15:54 +02:00
checkGLcall("glXMakeCurrent");
2005-06-23 13:05:24 +02:00
}
/* delete the old contxt*/
glXDestroyContext(oldDisplay, oldContext); /* Should this happen on an active context? seems a bad idea */
LEAVE_GL();
}
IWineD3DSwapChain_Release((IWineD3DSwapChain *)swapChainImpl);
2005-07-13 16:15:54 +02:00
2005-06-23 13:05:24 +02:00
}
2005-07-13 16:15:54 +02:00
2005-06-23 13:05:24 +02:00
/* TODO: The slow way, save the data to memory, create a new context for the destination window, transfer the data cleanup, it may be a good idea to the move this swapchain over to the using the target winows context so that it runs faster in feature. */
2005-07-13 16:15:54 +02:00
2005-06-23 13:05:24 +02:00
glXSwapBuffers(This->display, This->drawable); /* TODO: cycle through the swapchain buffers */
2005-07-13 16:15:54 +02:00
2005-06-23 13:05:24 +02:00
TRACE("glXSwapBuffers called, Starting new frame\n");
/* FPS support */
if (TRACE_ON(d3d_fps))
{
static long prev_time, frames;
DWORD time = GetTickCount();
frames++;
/* every 1.5 seconds */
if (time - prev_time > 1500) {
TRACE_(d3d_fps)("@ approx %.2ffps\n", 1000.0*frames/(time - prev_time));
prev_time = time;
frames = 0;
}
}
#if defined(FRAME_DEBUGGING)
{
if (GetFileAttributesA("C:\\D3DTRACE") != INVALID_FILE_ATTRIBUTES) {
if (!isOn) {
isOn = TRUE;
FIXME("Enabling D3D Trace\n");
__WINE_SET_DEBUGGING(__WINE_DBCL_TRACE, __wine_dbch_d3d, 1);
#if defined(SHOW_FRAME_MAKEUP)
FIXME("Singe Frame snapshots Starting\n");
isDumpingFrames = TRUE;
glClear(GL_COLOR_BUFFER_BIT);
#endif
#if defined(SINGLE_FRAME_DEBUGGING)
} else {
#if defined(SHOW_FRAME_MAKEUP)
FIXME("Singe Frame snapshots Finishing\n");
isDumpingFrames = FALSE;
#endif
FIXME("Singe Frame trace complete\n");
DeleteFileA("C:\\D3DTRACE");
__WINE_SET_DEBUGGING(__WINE_DBCL_TRACE, __wine_dbch_d3d, 0);
#endif
}
} else {
if (isOn) {
isOn = FALSE;
#if defined(SHOW_FRAME_MAKEUP)
FIXME("Single Frame snapshots Finishing\n");
isDumpingFrames = FALSE;
#endif
FIXME("Disabling D3D Trace\n");
__WINE_SET_DEBUGGING(__WINE_DBCL_TRACE, __wine_dbch_d3d, 0);
}
}
}
#endif
LEAVE_GL();
/* Although this is not strictly required, a simple demo showed this does occur
on (at least non-debug) d3d */
if (This->presentParms.SwapEffect & D3DSWAPEFFECT_DISCARD) {
2005-07-13 16:15:54 +02:00
TRACE("Clearing\n");
2005-06-23 13:05:24 +02:00
IWineD3DDevice_Clear((IWineD3DDevice*)This->wineD3DDevice, 0, NULL, D3DCLEAR_STENCIL|D3DCLEAR_ZBUFFER|D3DCLEAR_TARGET, 0x00, 1.0, 0);
}
TRACE("returning\n");
return D3D_OK;
}
HRESULT WINAPI IWineD3DSwapChainImpl_GetFrontBufferData(IWineD3DSwapChain *iface, IWineD3DSurface *pDestSurface) {
IWineD3DSwapChainImpl *This = (IWineD3DSwapChainImpl *)iface;
WINED3DFORMAT d3dformat;
2005-08-26 10:53:31 +02:00
UINT width;
UINT height;
WINED3DSURFACE_DESC desc;
glDescriptor *glDescription;
2005-07-13 16:15:54 +02:00
2005-06-23 13:05:24 +02:00
TRACE("(%p) : iface(%p) pDestSurface(%p) \n", This, iface, pDestSurface);
ENTER_GL();
desc.Width = &width;
desc.Height = &height;
desc.Format = &d3dformat;
#if 0 /* TODO: make sure that this swapchains context is active */
IWineD3DDevice_ActivateSwapChainContext(This->wineD3DDevice, iface);
#endif
IWineD3DSurface_GetDesc(pDestSurface, &desc);
/* make sure that the front buffer is the active read buffer */
2005-06-23 13:05:24 +02:00
glReadBuffer(GL_FRONT);
/* Read the pixels from the buffer into the surfaces memory */
IWineD3DSurface_GetGlDesc(pDestSurface, &glDescription);
glReadPixels(glDescription->target,
glDescription->level,
width,
height,
glDescription->glFormat,
glDescription->glType,
(void *)IWineD3DSurface_GetData(pDestSurface));
2005-06-23 13:05:24 +02:00
LEAVE_GL();
return D3D_OK;
}
HRESULT WINAPI IWineD3DSwapChainImpl_GetBackBuffer(IWineD3DSwapChain *iface, UINT iBackBuffer, D3DBACKBUFFER_TYPE Type, IWineD3DSurface **ppBackBuffer) {
2005-07-13 16:15:54 +02:00
IWineD3DSwapChainImpl *This = (IWineD3DSwapChainImpl *)iface;
*ppBackBuffer = This->backBuffer;
2005-06-23 13:05:24 +02:00
TRACE("(%p) : BackBuf %d Type %d returning %p\n", This, iBackBuffer, Type, *ppBackBuffer);
if (iBackBuffer > This->presentParms.BackBufferCount - 1) {
FIXME("Only one backBuffer currently supported\n");
return D3DERR_INVALIDCALL;
}
/* Note inc ref on returned surface */
2005-07-13 16:15:54 +02:00
IWineD3DSurface_AddRef(*ppBackBuffer);
2005-06-23 13:05:24 +02:00
return D3D_OK;
2005-07-13 16:15:54 +02:00
2005-06-23 13:05:24 +02:00
}
HRESULT WINAPI IWineD3DSwapChainImpl_GetRasterStatus(IWineD3DSwapChain *iface, D3DRASTER_STATUS*pRasterStatus) {
2005-07-13 16:15:54 +02:00
IWineD3DSwapChainImpl *This = (IWineD3DSwapChainImpl *)iface;
static BOOL showFixmes = TRUE;
2005-06-23 13:05:24 +02:00
pRasterStatus->InVBlank = TRUE;
pRasterStatus->ScanLine = 0;
/* No openGL equivalent */
if(showFixmes) {
FIXME("(%p) : stub (once)\n", This);
showFixmes = FALSE;
}
2005-06-23 13:05:24 +02:00
return D3D_OK;
}
HRESULT WINAPI IWineD3DSwapChainImpl_GetDisplayMode(IWineD3DSwapChain *iface, D3DDISPLAYMODE*pMode) {
2005-07-13 16:15:54 +02:00
IWineD3DSwapChainImpl *This = (IWineD3DSwapChainImpl *)iface;
2005-06-23 13:05:24 +02:00
HDC hdc;
int bpp = 0;
pMode->Width = GetSystemMetrics(SM_CXSCREEN);
pMode->Height = GetSystemMetrics(SM_CYSCREEN);
pMode->RefreshRate = 85; /* FIXME: How to identify? */
hdc = CreateDCA("DISPLAY", NULL, NULL, NULL);
bpp = GetDeviceCaps(hdc, BITSPIXEL);
DeleteDC(hdc);
switch (bpp) {
case 8: pMode->Format = D3DFMT_R8G8B8; break;
case 16: pMode->Format = D3DFMT_R5G6B5; break;
case 24: /*pMode->Format = D3DFMT_R8G8B8; break; */ /* 32bpp and 24bpp can be aliased for X */
case 32: pMode->Format = D3DFMT_A8R8G8B8; break;
2005-07-13 16:15:54 +02:00
default:
2005-06-23 13:05:24 +02:00
FIXME("Unrecognized display mode format\n");
pMode->Format = D3DFMT_UNKNOWN;
}
TRACE("(%p) : returning w(%d) h(%d) rr(%d) fmt(%u,%s)\n", This, pMode->Width, pMode->Height, pMode->RefreshRate,
pMode->Format, debug_d3dformat(pMode->Format));
2005-07-13 16:15:54 +02:00
return D3D_OK;
2005-06-23 13:05:24 +02:00
}
HRESULT WINAPI IWineD3DSwapChainImpl_GetDevice(IWineD3DSwapChain *iface, IWineD3DDevice**ppDevice) {
2005-07-13 16:15:54 +02:00
IWineD3DSwapChainImpl *This = (IWineD3DSwapChainImpl *)iface;
2005-06-23 13:05:24 +02:00
*ppDevice = (IWineD3DDevice *) This->wineD3DDevice;
2005-07-13 16:15:54 +02:00
/* Note Calling this method will increase the internal reference count
2005-06-23 13:05:24 +02:00
on the IDirect3DDevice9 interface. */
IWineD3DDevice_AddRef(*ppDevice);
TRACE("(%p) : returning %p\n", This, *ppDevice);
return D3D_OK;
}
HRESULT WINAPI IWineD3DSwapChainImpl_GetPresentParameters(IWineD3DSwapChain *iface, D3DPRESENT_PARAMETERS *pPresentationParameters) {
IWineD3DSwapChainImpl *This = (IWineD3DSwapChainImpl *)iface;
2005-07-13 16:15:54 +02:00
FIXME("(%p) : copy\n", This);
2005-06-23 13:05:24 +02:00
memcpy(pPresentationParameters, &This->presentParms, sizeof(D3DPRESENT_PARAMETERS));
return D3D_OK;
}
HRESULT WINAPI IWineD3DSwapChainImpl_SetGammaRamp(IWineD3DSwapChain *iface, DWORD Flags, CONST D3DGAMMARAMP *pRamp){
IWineD3DSwapChainImpl *This = (IWineD3DSwapChainImpl *)iface;
HDC hDC;
TRACE("(%p) : pRamp@%p flags(%ld) \n", This, pRamp, Flags);
hDC = GetDC(This->win_handle);
SetDeviceGammaRamp(hDC, (LPVOID)pRamp);
ReleaseDC(This->win_handle, hDC);
return D3D_OK;
2005-07-13 16:15:54 +02:00
2005-06-23 13:05:24 +02:00
}
HRESULT WINAPI IWineD3DSwapChainImpl_GetGammaRamp(IWineD3DSwapChain *iface, D3DGAMMARAMP *pRamp){
IWineD3DSwapChainImpl *This = (IWineD3DSwapChainImpl *)iface;
HDC hDC;
TRACE("(%p) : pRamp@%p\n", This, pRamp);
hDC = GetDC(This->win_handle);
GetDeviceGammaRamp(hDC, pRamp);
ReleaseDC(This->win_handle, hDC);
return D3D_OK;
2005-07-13 16:15:54 +02:00
2005-06-23 13:05:24 +02:00
}
IWineD3DSwapChainVtbl IWineD3DSwapChain_Vtbl =
{
2005-07-13 16:15:54 +02:00
/* IUnknown */
2005-06-23 13:05:24 +02:00
IWineD3DSwapChainImpl_QueryInterface,
IWineD3DSwapChainImpl_AddRef,
IWineD3DSwapChainImpl_Release,
2005-07-13 16:15:54 +02:00
/* IWineD3DSwapChain */
2005-06-23 13:05:24 +02:00
IWineD3DSwapChainImpl_GetParent,
IWineD3DSwapChainImpl_GetDevice,
IWineD3DSwapChainImpl_Present,
IWineD3DSwapChainImpl_GetFrontBufferData,
IWineD3DSwapChainImpl_GetBackBuffer,
IWineD3DSwapChainImpl_GetRasterStatus,
IWineD3DSwapChainImpl_GetDisplayMode,
IWineD3DSwapChainImpl_GetPresentParameters,
IWineD3DSwapChainImpl_SetGammaRamp,
2005-07-13 16:15:54 +02:00
IWineD3DSwapChainImpl_GetGammaRamp
2005-06-23 13:05:24 +02:00
};