Home | Info | Community | Development | myReactOS | Contact Us
ReactOS Development > Doxygenquery.c
Go to the documentation of this file.
00001 /* 00002 * Copyright 2005 Oliver Stieber 00003 * Copyright 2007-2008 Stefan Dösinger for CodeWeavers 00004 * Copyright 2009-2010 Henri Verbeet for CodeWeavers. 00005 * 00006 * This library is free software; you can redistribute it and/or 00007 * modify it under the terms of the GNU Lesser General Public 00008 * License as published by the Free Software Foundation; either 00009 * version 2.1 of the License, or (at your option) any later version. 00010 * 00011 * This library is distributed in the hope that it will be useful, 00012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 00013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 00014 * Lesser General Public License for more details. 00015 * 00016 * You should have received a copy of the GNU Lesser General Public 00017 * License along with this library; if not, write to the Free Software 00018 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA 00019 */ 00020 00021 00022 #include "config.h" 00023 #include "wined3d_private.h" 00024 00025 WINE_DEFAULT_DEBUG_CHANNEL(d3d); 00026 00027 BOOL wined3d_event_query_supported(const struct wined3d_gl_info *gl_info) 00028 { 00029 return gl_info->supported[ARB_SYNC] || gl_info->supported[NV_FENCE] || gl_info->supported[APPLE_FENCE]; 00030 } 00031 00032 void wined3d_event_query_destroy(struct wined3d_event_query *query) 00033 { 00034 if (query->context) context_free_event_query(query); 00035 HeapFree(GetProcessHeap(), 0, query); 00036 } 00037 00038 static enum wined3d_event_query_result wined3d_event_query_test(const struct wined3d_event_query *query, 00039 const struct wined3d_device *device) 00040 { 00041 struct wined3d_context *context; 00042 const struct wined3d_gl_info *gl_info; 00043 enum wined3d_event_query_result ret; 00044 BOOL fence_result; 00045 00046 TRACE("(%p) : device %p\n", query, device); 00047 00048 if (!query->context) 00049 { 00050 TRACE("Query not started\n"); 00051 return WINED3D_EVENT_QUERY_NOT_STARTED; 00052 } 00053 00054 if (!query->context->gl_info->supported[ARB_SYNC] && query->context->tid != GetCurrentThreadId()) 00055 { 00056 WARN("Event query tested from wrong thread\n"); 00057 return WINED3D_EVENT_QUERY_WRONG_THREAD; 00058 } 00059 00060 context = context_acquire(device, query->context->current_rt); 00061 gl_info = context->gl_info; 00062 00063 ENTER_GL(); 00064 00065 if (gl_info->supported[ARB_SYNC]) 00066 { 00067 GLenum gl_ret = GL_EXTCALL(glClientWaitSync(query->object.sync, 0, 0)); 00068 checkGLcall("glClientWaitSync"); 00069 00070 switch (gl_ret) 00071 { 00072 case GL_ALREADY_SIGNALED: 00073 case GL_CONDITION_SATISFIED: 00074 ret = WINED3D_EVENT_QUERY_OK; 00075 break; 00076 00077 case GL_TIMEOUT_EXPIRED: 00078 ret = WINED3D_EVENT_QUERY_WAITING; 00079 break; 00080 00081 case GL_WAIT_FAILED: 00082 default: 00083 ERR("glClientWaitSync returned %#x.\n", gl_ret); 00084 ret = WINED3D_EVENT_QUERY_ERROR; 00085 } 00086 } 00087 else if (gl_info->supported[APPLE_FENCE]) 00088 { 00089 fence_result = GL_EXTCALL(glTestFenceAPPLE(query->object.id)); 00090 checkGLcall("glTestFenceAPPLE"); 00091 if (fence_result) ret = WINED3D_EVENT_QUERY_OK; 00092 else ret = WINED3D_EVENT_QUERY_WAITING; 00093 } 00094 else if (gl_info->supported[NV_FENCE]) 00095 { 00096 fence_result = GL_EXTCALL(glTestFenceNV(query->object.id)); 00097 checkGLcall("glTestFenceNV"); 00098 if (fence_result) ret = WINED3D_EVENT_QUERY_OK; 00099 else ret = WINED3D_EVENT_QUERY_WAITING; 00100 } 00101 else 00102 { 00103 ERR("Event query created despite lack of GL support\n"); 00104 ret = WINED3D_EVENT_QUERY_ERROR; 00105 } 00106 00107 LEAVE_GL(); 00108 00109 context_release(context); 00110 return ret; 00111 } 00112 00113 enum wined3d_event_query_result wined3d_event_query_finish(const struct wined3d_event_query *query, 00114 const struct wined3d_device *device) 00115 { 00116 struct wined3d_context *context; 00117 const struct wined3d_gl_info *gl_info; 00118 enum wined3d_event_query_result ret; 00119 00120 TRACE("(%p)\n", query); 00121 00122 if (!query->context) 00123 { 00124 TRACE("Query not started\n"); 00125 return WINED3D_EVENT_QUERY_NOT_STARTED; 00126 } 00127 gl_info = query->context->gl_info; 00128 00129 if (query->context->tid != GetCurrentThreadId() && !gl_info->supported[ARB_SYNC]) 00130 { 00131 /* A glFinish does not reliably wait for draws in other contexts. The caller has 00132 * to find its own way to cope with the thread switch 00133 */ 00134 WARN("Event query finished from wrong thread\n"); 00135 return WINED3D_EVENT_QUERY_WRONG_THREAD; 00136 } 00137 00138 context = context_acquire(device, query->context->current_rt); 00139 00140 ENTER_GL(); 00141 if (gl_info->supported[ARB_SYNC]) 00142 { 00143 GLenum gl_ret = GL_EXTCALL(glClientWaitSync(query->object.sync, 0, ~(GLuint64)0)); 00144 checkGLcall("glClientWaitSync"); 00145 00146 switch (gl_ret) 00147 { 00148 case GL_ALREADY_SIGNALED: 00149 case GL_CONDITION_SATISFIED: 00150 ret = WINED3D_EVENT_QUERY_OK; 00151 break; 00152 00153 /* We don't expect a timeout for a ~584 year wait */ 00154 default: 00155 ERR("glClientWaitSync returned %#x.\n", gl_ret); 00156 ret = WINED3D_EVENT_QUERY_ERROR; 00157 } 00158 } 00159 else if (context->gl_info->supported[APPLE_FENCE]) 00160 { 00161 GL_EXTCALL(glFinishFenceAPPLE(query->object.id)); 00162 checkGLcall("glFinishFenceAPPLE"); 00163 ret = WINED3D_EVENT_QUERY_OK; 00164 } 00165 else if (context->gl_info->supported[NV_FENCE]) 00166 { 00167 GL_EXTCALL(glFinishFenceNV(query->object.id)); 00168 checkGLcall("glFinishFenceNV"); 00169 ret = WINED3D_EVENT_QUERY_OK; 00170 } 00171 else 00172 { 00173 ERR("Event query created without GL support\n"); 00174 ret = WINED3D_EVENT_QUERY_ERROR; 00175 } 00176 LEAVE_GL(); 00177 00178 context_release(context); 00179 return ret; 00180 } 00181 00182 void wined3d_event_query_issue(struct wined3d_event_query *query, const struct wined3d_device *device) 00183 { 00184 const struct wined3d_gl_info *gl_info; 00185 struct wined3d_context *context; 00186 00187 if (query->context) 00188 { 00189 if (!query->context->gl_info->supported[ARB_SYNC] && query->context->tid != GetCurrentThreadId()) 00190 { 00191 context_free_event_query(query); 00192 context = context_acquire(device, NULL); 00193 context_alloc_event_query(context, query); 00194 } 00195 else 00196 { 00197 context = context_acquire(device, query->context->current_rt); 00198 } 00199 } 00200 else 00201 { 00202 context = context_acquire(device, NULL); 00203 context_alloc_event_query(context, query); 00204 } 00205 00206 gl_info = context->gl_info; 00207 00208 ENTER_GL(); 00209 00210 if (gl_info->supported[ARB_SYNC]) 00211 { 00212 if (query->object.sync) GL_EXTCALL(glDeleteSync(query->object.sync)); 00213 checkGLcall("glDeleteSync"); 00214 query->object.sync = GL_EXTCALL(glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0)); 00215 checkGLcall("glFenceSync"); 00216 } 00217 else if (gl_info->supported[APPLE_FENCE]) 00218 { 00219 GL_EXTCALL(glSetFenceAPPLE(query->object.id)); 00220 checkGLcall("glSetFenceAPPLE"); 00221 } 00222 else if (gl_info->supported[NV_FENCE]) 00223 { 00224 GL_EXTCALL(glSetFenceNV(query->object.id, GL_ALL_COMPLETED_NV)); 00225 checkGLcall("glSetFenceNV"); 00226 } 00227 00228 LEAVE_GL(); 00229 00230 context_release(context); 00231 } 00232 00233 ULONG CDECL wined3d_query_incref(struct wined3d_query *query) 00234 { 00235 ULONG refcount = InterlockedIncrement(&query->ref); 00236 00237 TRACE("%p increasing refcount to %u.\n", query, refcount); 00238 00239 return refcount; 00240 } 00241 00242 ULONG CDECL wined3d_query_decref(struct wined3d_query *query) 00243 { 00244 ULONG refcount = InterlockedDecrement(&query->ref); 00245 00246 TRACE("%p decreasing refcount to %u.\n", query, refcount); 00247 00248 if (!refcount) 00249 { 00250 /* Queries are specific to the GL context that created them. Not 00251 * deleting the query will obviously leak it, but that's still better 00252 * than potentially deleting a different query with the same id in this 00253 * context, and (still) leaking the actual query. */ 00254 if (query->type == WINED3D_QUERY_TYPE_EVENT) 00255 { 00256 struct wined3d_event_query *event_query = query->extendedData; 00257 if (event_query) wined3d_event_query_destroy(event_query); 00258 } 00259 else if (query->type == WINED3D_QUERY_TYPE_OCCLUSION) 00260 { 00261 struct wined3d_occlusion_query *oq = query->extendedData; 00262 00263 if (oq->context) context_free_occlusion_query(oq); 00264 HeapFree(GetProcessHeap(), 0, query->extendedData); 00265 } 00266 00267 HeapFree(GetProcessHeap(), 0, query); 00268 } 00269 00270 return refcount; 00271 } 00272 00273 HRESULT CDECL wined3d_query_get_data(struct wined3d_query *query, 00274 void *data, UINT data_size, DWORD flags) 00275 { 00276 TRACE("query %p, data %p, data_size %u, flags %#x.\n", 00277 query, data, data_size, flags); 00278 00279 return query->query_ops->query_get_data(query, data, data_size, flags); 00280 } 00281 00282 UINT CDECL wined3d_query_get_data_size(const struct wined3d_query *query) 00283 { 00284 TRACE("query %p.\n", query); 00285 00286 return query->data_size; 00287 } 00288 00289 HRESULT CDECL wined3d_query_issue(struct wined3d_query *query, DWORD flags) 00290 { 00291 TRACE("query %p, flags %#x.\n", query, flags); 00292 00293 return query->query_ops->query_issue(query, flags); 00294 } 00295 00296 static HRESULT wined3d_occlusion_query_ops_get_data(struct wined3d_query *query, 00297 void *pData, DWORD dwSize, DWORD flags) 00298 { 00299 struct wined3d_occlusion_query *oq = query->extendedData; 00300 struct wined3d_device *device = query->device; 00301 const struct wined3d_gl_info *gl_info = &device->adapter->gl_info; 00302 struct wined3d_context *context; 00303 DWORD* data = pData; 00304 GLuint available; 00305 GLuint samples; 00306 HRESULT res; 00307 00308 TRACE("(%p) : type D3DQUERY_OCCLUSION, pData %p, dwSize %#x, flags %#x.\n", query, pData, dwSize, flags); 00309 00310 if (!oq->context) 00311 query->state = QUERY_CREATED; 00312 00313 if (query->state == QUERY_CREATED) 00314 { 00315 /* D3D allows GetData on a new query, OpenGL doesn't. So just invent the data ourselves */ 00316 TRACE("Query wasn't yet started, returning S_OK\n"); 00317 if(data) *data = 0; 00318 return S_OK; 00319 } 00320 00321 if (query->state == QUERY_BUILDING) 00322 { 00323 /* Msdn says this returns an error, but our tests show that S_FALSE is returned */ 00324 TRACE("Query is building, returning S_FALSE\n"); 00325 return S_FALSE; 00326 } 00327 00328 if (!gl_info->supported[ARB_OCCLUSION_QUERY]) 00329 { 00330 WARN("%p Occlusion queries not supported. Returning 1.\n", query); 00331 *data = 1; 00332 return S_OK; 00333 } 00334 00335 if (oq->context->tid != GetCurrentThreadId()) 00336 { 00337 FIXME("%p Wrong thread, returning 1.\n", query); 00338 *data = 1; 00339 return S_OK; 00340 } 00341 00342 context = context_acquire(query->device, oq->context->current_rt); 00343 00344 ENTER_GL(); 00345 00346 GL_EXTCALL(glGetQueryObjectuivARB(oq->id, GL_QUERY_RESULT_AVAILABLE_ARB, &available)); 00347 checkGLcall("glGetQueryObjectuivARB(GL_QUERY_RESULT_AVAILABLE)"); 00348 TRACE("available %#x.\n", available); 00349 00350 if (available) 00351 { 00352 if (data) 00353 { 00354 GL_EXTCALL(glGetQueryObjectuivARB(oq->id, GL_QUERY_RESULT_ARB, &samples)); 00355 checkGLcall("glGetQueryObjectuivARB(GL_QUERY_RESULT)"); 00356 TRACE("Returning %d samples.\n", samples); 00357 *data = samples; 00358 } 00359 res = S_OK; 00360 } 00361 else 00362 { 00363 res = S_FALSE; 00364 } 00365 00366 LEAVE_GL(); 00367 00368 context_release(context); 00369 00370 return res; 00371 } 00372 00373 static HRESULT wined3d_event_query_ops_get_data(struct wined3d_query *query, 00374 void *pData, DWORD dwSize, DWORD flags) 00375 { 00376 struct wined3d_event_query *event_query = query->extendedData; 00377 BOOL *data = pData; 00378 enum wined3d_event_query_result ret; 00379 00380 TRACE("query %p, pData %p, dwSize %#x, flags %#x.\n", query, pData, dwSize, flags); 00381 00382 if (!pData || !dwSize) return S_OK; 00383 if (!event_query) 00384 { 00385 WARN("Event query not supported by GL, reporting GPU idle.\n"); 00386 *data = TRUE; 00387 return S_OK; 00388 } 00389 00390 ret = wined3d_event_query_test(event_query, query->device); 00391 switch(ret) 00392 { 00393 case WINED3D_EVENT_QUERY_OK: 00394 case WINED3D_EVENT_QUERY_NOT_STARTED: 00395 *data = TRUE; 00396 break; 00397 00398 case WINED3D_EVENT_QUERY_WAITING: 00399 *data = FALSE; 00400 break; 00401 00402 case WINED3D_EVENT_QUERY_WRONG_THREAD: 00403 FIXME("(%p) Wrong thread, reporting GPU idle.\n", query); 00404 *data = TRUE; 00405 break; 00406 00407 case WINED3D_EVENT_QUERY_ERROR: 00408 ERR("The GL event query failed, returning D3DERR_INVALIDCALL\n"); 00409 return WINED3DERR_INVALIDCALL; 00410 } 00411 00412 return S_OK; 00413 } 00414 00415 enum wined3d_query_type CDECL wined3d_query_get_type(const struct wined3d_query *query) 00416 { 00417 TRACE("query %p.\n", query); 00418 00419 return query->type; 00420 } 00421 00422 static HRESULT wined3d_event_query_ops_issue(struct wined3d_query *query, DWORD flags) 00423 { 00424 TRACE("query %p, flags %#x.\n", query, flags); 00425 00426 TRACE("(%p) : flags %#x, type D3DQUERY_EVENT\n", query, flags); 00427 if (flags & WINED3DISSUE_END) 00428 { 00429 struct wined3d_event_query *event_query = query->extendedData; 00430 00431 /* Faked event query support */ 00432 if (!event_query) return WINED3D_OK; 00433 00434 wined3d_event_query_issue(event_query, query->device); 00435 } 00436 else if (flags & WINED3DISSUE_BEGIN) 00437 { 00438 /* Started implicitly at device creation */ 00439 ERR("Event query issued with START flag - what to do?\n"); 00440 } 00441 00442 if (flags & WINED3DISSUE_BEGIN) 00443 query->state = QUERY_BUILDING; 00444 else 00445 query->state = QUERY_SIGNALLED; 00446 00447 return WINED3D_OK; 00448 } 00449 00450 static HRESULT wined3d_occlusion_query_ops_issue(struct wined3d_query *query, DWORD flags) 00451 { 00452 struct wined3d_device *device = query->device; 00453 const struct wined3d_gl_info *gl_info = &device->adapter->gl_info; 00454 00455 TRACE("query %p, flags %#x.\n", query, flags); 00456 00457 if (gl_info->supported[ARB_OCCLUSION_QUERY]) 00458 { 00459 struct wined3d_occlusion_query *oq = query->extendedData; 00460 struct wined3d_context *context; 00461 00462 /* This is allowed according to msdn and our tests. Reset the query and restart */ 00463 if (flags & WINED3DISSUE_BEGIN) 00464 { 00465 if (query->state == QUERY_BUILDING) 00466 { 00467 if (oq->context->tid != GetCurrentThreadId()) 00468 { 00469 FIXME("Wrong thread, can't restart query.\n"); 00470 00471 context_free_occlusion_query(oq); 00472 context = context_acquire(query->device, NULL); 00473 context_alloc_occlusion_query(context, oq); 00474 } 00475 else 00476 { 00477 context = context_acquire(query->device, oq->context->current_rt); 00478 00479 ENTER_GL(); 00480 GL_EXTCALL(glEndQueryARB(GL_SAMPLES_PASSED_ARB)); 00481 checkGLcall("glEndQuery()"); 00482 LEAVE_GL(); 00483 } 00484 } 00485 else 00486 { 00487 if (oq->context) context_free_occlusion_query(oq); 00488 context = context_acquire(query->device, NULL); 00489 context_alloc_occlusion_query(context, oq); 00490 } 00491 00492 ENTER_GL(); 00493 GL_EXTCALL(glBeginQueryARB(GL_SAMPLES_PASSED_ARB, oq->id)); 00494 checkGLcall("glBeginQuery()"); 00495 LEAVE_GL(); 00496 00497 context_release(context); 00498 } 00499 if (flags & WINED3DISSUE_END) 00500 { 00501 /* Msdn says _END on a non-building occlusion query returns an error, but 00502 * our tests show that it returns OK. But OpenGL doesn't like it, so avoid 00503 * generating an error 00504 */ 00505 if (query->state == QUERY_BUILDING) 00506 { 00507 if (oq->context->tid != GetCurrentThreadId()) 00508 { 00509 FIXME("Wrong thread, can't end query.\n"); 00510 } 00511 else 00512 { 00513 context = context_acquire(query->device, oq->context->current_rt); 00514 00515 ENTER_GL(); 00516 GL_EXTCALL(glEndQueryARB(GL_SAMPLES_PASSED_ARB)); 00517 checkGLcall("glEndQuery()"); 00518 LEAVE_GL(); 00519 00520 context_release(context); 00521 } 00522 } 00523 } 00524 } 00525 else 00526 { 00527 FIXME("%p Occlusion queries not supported.\n", query); 00528 } 00529 00530 if (flags & WINED3DISSUE_BEGIN) 00531 query->state = QUERY_BUILDING; 00532 else 00533 query->state = QUERY_SIGNALLED; 00534 00535 return WINED3D_OK; /* can be WINED3DERR_INVALIDCALL. */ 00536 } 00537 00538 static const struct wined3d_query_ops event_query_ops = 00539 { 00540 wined3d_event_query_ops_get_data, 00541 wined3d_event_query_ops_issue, 00542 }; 00543 00544 static const struct wined3d_query_ops occlusion_query_ops = 00545 { 00546 wined3d_occlusion_query_ops_get_data, 00547 wined3d_occlusion_query_ops_issue, 00548 }; 00549 00550 static HRESULT query_init(struct wined3d_query *query, struct wined3d_device *device, enum wined3d_query_type type) 00551 { 00552 const struct wined3d_gl_info *gl_info = &device->adapter->gl_info; 00553 00554 switch (type) 00555 { 00556 case WINED3D_QUERY_TYPE_OCCLUSION: 00557 TRACE("Occlusion query.\n"); 00558 if (!gl_info->supported[ARB_OCCLUSION_QUERY]) 00559 { 00560 WARN("Unsupported in local OpenGL implementation: ARB_OCCLUSION_QUERY.\n"); 00561 return WINED3DERR_NOTAVAILABLE; 00562 } 00563 query->query_ops = &occlusion_query_ops; 00564 query->data_size = sizeof(DWORD); 00565 query->extendedData = HeapAlloc(GetProcessHeap(), 0, sizeof(struct wined3d_occlusion_query)); 00566 if (!query->extendedData) 00567 { 00568 ERR("Failed to allocate occlusion query extended data.\n"); 00569 return E_OUTOFMEMORY; 00570 } 00571 ((struct wined3d_occlusion_query *)query->extendedData)->context = NULL; 00572 break; 00573 00574 case WINED3D_QUERY_TYPE_EVENT: 00575 TRACE("Event query.\n"); 00576 if (!wined3d_event_query_supported(gl_info)) 00577 { 00578 /* Half-Life 2 needs this query. It does not render the main 00579 * menu correctly otherwise. Pretend to support it, faking 00580 * this query does not do much harm except potentially 00581 * lowering performance. */ 00582 FIXME("Event query: Unimplemented, but pretending to be supported.\n"); 00583 } 00584 query->query_ops = &event_query_ops; 00585 query->data_size = sizeof(BOOL); 00586 query->extendedData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(struct wined3d_event_query)); 00587 if (!query->extendedData) 00588 { 00589 ERR("Failed to allocate event query memory.\n"); 00590 return E_OUTOFMEMORY; 00591 } 00592 break; 00593 00594 case WINED3D_QUERY_TYPE_VCACHE: 00595 case WINED3D_QUERY_TYPE_RESOURCE_MANAGER: 00596 case WINED3D_QUERY_TYPE_VERTEX_STATS: 00597 case WINED3D_QUERY_TYPE_TIMESTAMP: 00598 case WINED3D_QUERY_TYPE_TIMESTAMP_DISJOINT: 00599 case WINED3D_QUERY_TYPE_TIMESTAMP_FREQ: 00600 case WINED3D_QUERY_TYPE_PIPELINE_TIMINGS: 00601 case WINED3D_QUERY_TYPE_INTERFACE_TIMINGS: 00602 case WINED3D_QUERY_TYPE_VERTEX_TIMINGS: 00603 case WINED3D_QUERY_TYPE_PIXEL_TIMINGS: 00604 case WINED3D_QUERY_TYPE_BANDWIDTH_TIMINGS: 00605 case WINED3D_QUERY_TYPE_CACHE_UTILIZATION: 00606 default: 00607 FIXME("Unhandled query type %#x.\n", type); 00608 return WINED3DERR_NOTAVAILABLE; 00609 } 00610 00611 query->type = type; 00612 query->state = QUERY_CREATED; 00613 query->device = device; 00614 query->ref = 1; 00615 00616 return WINED3D_OK; 00617 } 00618 00619 HRESULT CDECL wined3d_query_create(struct wined3d_device *device, 00620 enum wined3d_query_type type, struct wined3d_query **query) 00621 { 00622 struct wined3d_query *object; 00623 HRESULT hr; 00624 00625 TRACE("device %p, type %#x, query %p.\n", device, type, query); 00626 00627 object = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*object)); 00628 if (!object) 00629 { 00630 ERR("Failed to allocate query memory.\n"); 00631 return E_OUTOFMEMORY; 00632 } 00633 00634 hr = query_init(object, device, type); 00635 if (FAILED(hr)) 00636 { 00637 WARN("Failed to initialize query, hr %#x.\n", hr); 00638 HeapFree(GetProcessHeap(), 0, object); 00639 return hr; 00640 } 00641 00642 TRACE("Created query %p.\n", object); 00643 *query = object; 00644 00645 return WINED3D_OK; 00646 } Generated on Sat May 26 2012 04:15:57 for ReactOS by
1.7.6.1
|