OLD | NEW |
(Empty) | |
| 1 /* |
| 2 * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. |
| 3 * |
| 4 * Use of this source code is governed by a BSD-style license |
| 5 * that can be found in the LICENSE file in the root of the source |
| 6 * tree. An additional intellectual property rights grant can be found |
| 7 * in the file PATENTS. All contributing project authors may |
| 8 * be found in the AUTHORS file in the root of the source tree. |
| 9 */ |
| 10 |
| 11 // Own include file |
| 12 #include "webrtc/modules/video_render/windows/video_render_direct3d9.h" |
| 13 |
| 14 // System include files |
| 15 #include <windows.h> |
| 16 |
| 17 // WebRtc include files |
| 18 #include "webrtc/common_video/libyuv/include/webrtc_libyuv.h" |
| 19 #include "webrtc/system_wrappers/include/critical_section_wrapper.h" |
| 20 #include "webrtc/system_wrappers/include/event_wrapper.h" |
| 21 #include "webrtc/system_wrappers/include/trace.h" |
| 22 |
| 23 namespace webrtc { |
| 24 |
| 25 // A structure for our custom vertex type |
| 26 struct CUSTOMVERTEX |
| 27 { |
| 28 FLOAT x, y, z; |
| 29 DWORD color; // The vertex color |
| 30 FLOAT u, v; |
| 31 }; |
| 32 |
| 33 // Our custom FVF, which describes our custom vertex structure |
| 34 #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ|D3DFVF_DIFFUSE|D3DFVF_TEX1) |
| 35 |
| 36 /* |
| 37 * |
| 38 * D3D9Channel |
| 39 * |
| 40 */ |
| 41 D3D9Channel::D3D9Channel(LPDIRECT3DDEVICE9 pd3DDevice, |
| 42 CriticalSectionWrapper* critSect, |
| 43 Trace* trace) : |
| 44 _width(0), |
| 45 _height(0), |
| 46 _pd3dDevice(pd3DDevice), |
| 47 _pTexture(NULL), |
| 48 _bufferIsUpdated(false), |
| 49 _critSect(critSect), |
| 50 _streamId(0), |
| 51 _zOrder(0), |
| 52 _startWidth(0), |
| 53 _startHeight(0), |
| 54 _stopWidth(0), |
| 55 _stopHeight(0) |
| 56 { |
| 57 |
| 58 } |
| 59 |
| 60 D3D9Channel::~D3D9Channel() |
| 61 { |
| 62 //release the texture |
| 63 if (_pTexture != NULL) |
| 64 { |
| 65 _pTexture->Release(); |
| 66 _pTexture = NULL; |
| 67 } |
| 68 } |
| 69 |
| 70 void D3D9Channel::SetStreamSettings(uint16_t streamId, |
| 71 uint32_t zOrder, |
| 72 float startWidth, |
| 73 float startHeight, |
| 74 float stopWidth, |
| 75 float stopHeight) |
| 76 { |
| 77 _streamId = streamId; |
| 78 _zOrder = zOrder; |
| 79 _startWidth = startWidth; |
| 80 _startHeight = startHeight; |
| 81 _stopWidth = stopWidth; |
| 82 _stopHeight = stopHeight; |
| 83 } |
| 84 |
| 85 int D3D9Channel::GetStreamSettings(uint16_t streamId, |
| 86 uint32_t& zOrder, |
| 87 float& startWidth, |
| 88 float& startHeight, |
| 89 float& stopWidth, |
| 90 float& stopHeight) |
| 91 { |
| 92 streamId = _streamId; |
| 93 zOrder = _zOrder; |
| 94 startWidth = _startWidth; |
| 95 startHeight = _startHeight; |
| 96 stopWidth = _stopWidth; |
| 97 stopHeight = _stopHeight; |
| 98 return 0; |
| 99 } |
| 100 |
| 101 int D3D9Channel::GetTextureWidth() |
| 102 { |
| 103 return _width; |
| 104 } |
| 105 |
| 106 int D3D9Channel::GetTextureHeight() |
| 107 { |
| 108 return _height; |
| 109 } |
| 110 |
| 111 // Called from video engine when a the frame size changed |
| 112 int D3D9Channel::FrameSizeChange(int width, int height, int numberOfStreams) |
| 113 { |
| 114 WEBRTC_TRACE(kTraceInfo, kTraceVideo, -1, |
| 115 "FrameSizeChange, wifth: %d, height: %d, streams: %d", width, |
| 116 height, numberOfStreams); |
| 117 |
| 118 CriticalSectionScoped cs(_critSect); |
| 119 _width = width; |
| 120 _height = height; |
| 121 |
| 122 //clean the previous texture |
| 123 if (_pTexture != NULL) |
| 124 { |
| 125 _pTexture->Release(); |
| 126 _pTexture = NULL; |
| 127 } |
| 128 |
| 129 HRESULT ret = E_POINTER; |
| 130 |
| 131 if (_pd3dDevice) |
| 132 ret = _pd3dDevice->CreateTexture(_width, _height, 1, 0, D3DFMT_A8R8G8B8, |
| 133 D3DPOOL_MANAGED, &_pTexture, NULL); |
| 134 |
| 135 if (FAILED(ret)) |
| 136 { |
| 137 _pTexture = NULL; |
| 138 return -1; |
| 139 } |
| 140 |
| 141 return 0; |
| 142 } |
| 143 |
| 144 int32_t D3D9Channel::RenderFrame(const uint32_t streamId, |
| 145 const VideoFrame& videoFrame) { |
| 146 CriticalSectionScoped cs(_critSect); |
| 147 if (_width != videoFrame.width() || _height != videoFrame.height()) |
| 148 { |
| 149 if (FrameSizeChange(videoFrame.width(), videoFrame.height(), 1) == -1) |
| 150 { |
| 151 return -1; |
| 152 } |
| 153 } |
| 154 return DeliverFrame(videoFrame); |
| 155 } |
| 156 |
| 157 // Called from video engine when a new frame should be rendered. |
| 158 int D3D9Channel::DeliverFrame(const VideoFrame& videoFrame) { |
| 159 WEBRTC_TRACE(kTraceStream, kTraceVideo, -1, |
| 160 "DeliverFrame to D3D9Channel"); |
| 161 |
| 162 CriticalSectionScoped cs(_critSect); |
| 163 |
| 164 // FIXME if _bufferIsUpdated is still true (not be renderred), do we want to |
| 165 // update the texture? probably not |
| 166 if (_bufferIsUpdated) { |
| 167 WEBRTC_TRACE(kTraceStream, kTraceVideo, -1, |
| 168 "Last frame hasn't been rendered yet. Drop this frame."); |
| 169 return -1; |
| 170 } |
| 171 |
| 172 if (!_pd3dDevice) { |
| 173 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 174 "D3D for rendering not initialized."); |
| 175 return -1; |
| 176 } |
| 177 |
| 178 if (!_pTexture) { |
| 179 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 180 "Texture for rendering not initialized."); |
| 181 return -1; |
| 182 } |
| 183 |
| 184 D3DLOCKED_RECT lr; |
| 185 |
| 186 if (FAILED(_pTexture->LockRect(0, &lr, NULL, 0))) { |
| 187 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 188 "Failed to lock a texture in D3D9 Channel."); |
| 189 return -1; |
| 190 } |
| 191 UCHAR* pRect = (UCHAR*) lr.pBits; |
| 192 |
| 193 ConvertFromI420(videoFrame, kARGB, 0, pRect); |
| 194 |
| 195 if (FAILED(_pTexture->UnlockRect(0))) { |
| 196 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 197 "Failed to unlock a texture in D3D9 Channel."); |
| 198 return -1; |
| 199 } |
| 200 |
| 201 _bufferIsUpdated = true; |
| 202 return 0; |
| 203 } |
| 204 |
| 205 // Called by d3d channel owner to indicate the frame/texture has been rendered o
ff |
| 206 int D3D9Channel::RenderOffFrame() |
| 207 { |
| 208 WEBRTC_TRACE(kTraceStream, kTraceVideo, -1, |
| 209 "Frame has been rendered to the screen."); |
| 210 CriticalSectionScoped cs(_critSect); |
| 211 _bufferIsUpdated = false; |
| 212 return 0; |
| 213 } |
| 214 |
| 215 // Called by d3d channel owner to check if the texture is updated |
| 216 int D3D9Channel::IsUpdated(bool& isUpdated) |
| 217 { |
| 218 CriticalSectionScoped cs(_critSect); |
| 219 isUpdated = _bufferIsUpdated; |
| 220 return 0; |
| 221 } |
| 222 |
| 223 // Called by d3d channel owner to get the texture |
| 224 LPDIRECT3DTEXTURE9 D3D9Channel::GetTexture() |
| 225 { |
| 226 CriticalSectionScoped cs(_critSect); |
| 227 return _pTexture; |
| 228 } |
| 229 |
| 230 int D3D9Channel::ReleaseTexture() |
| 231 { |
| 232 CriticalSectionScoped cs(_critSect); |
| 233 |
| 234 //release the texture |
| 235 if (_pTexture != NULL) |
| 236 { |
| 237 _pTexture->Release(); |
| 238 _pTexture = NULL; |
| 239 } |
| 240 _pd3dDevice = NULL; |
| 241 return 0; |
| 242 } |
| 243 |
| 244 int D3D9Channel::RecreateTexture(LPDIRECT3DDEVICE9 pd3DDevice) |
| 245 { |
| 246 CriticalSectionScoped cs(_critSect); |
| 247 |
| 248 _pd3dDevice = pd3DDevice; |
| 249 |
| 250 if (_pTexture != NULL) |
| 251 { |
| 252 _pTexture->Release(); |
| 253 _pTexture = NULL; |
| 254 } |
| 255 |
| 256 HRESULT ret; |
| 257 |
| 258 ret = _pd3dDevice->CreateTexture(_width, _height, 1, 0, D3DFMT_A8R8G8B8, |
| 259 D3DPOOL_MANAGED, &_pTexture, NULL); |
| 260 |
| 261 if (FAILED(ret)) |
| 262 { |
| 263 _pTexture = NULL; |
| 264 return -1; |
| 265 } |
| 266 |
| 267 return 0; |
| 268 } |
| 269 |
| 270 /* |
| 271 * |
| 272 * VideoRenderDirect3D9 |
| 273 * |
| 274 */ |
| 275 VideoRenderDirect3D9::VideoRenderDirect3D9(Trace* trace, |
| 276 HWND hWnd, |
| 277 bool fullScreen) : |
| 278 _refD3DCritsect(*CriticalSectionWrapper::CreateCriticalSection()), |
| 279 _trace(trace), |
| 280 _hWnd(hWnd), |
| 281 _fullScreen(fullScreen), |
| 282 _pTextureLogo(NULL), |
| 283 _pVB(NULL), |
| 284 _pd3dDevice(NULL), |
| 285 _pD3D(NULL), |
| 286 _d3dChannels(), |
| 287 _d3dZorder(), |
| 288 _screenUpdateEvent(NULL), |
| 289 _logoLeft(0), |
| 290 _logoTop(0), |
| 291 _logoRight(0), |
| 292 _logoBottom(0), |
| 293 _pd3dSurface(NULL), |
| 294 _totalMemory(0), |
| 295 _availableMemory(0) |
| 296 { |
| 297 _screenUpdateThread.reset(new rtc::PlatformThread( |
| 298 ScreenUpdateThreadProc, this, "ScreenUpdateThread")); |
| 299 _screenUpdateEvent = EventTimerWrapper::Create(); |
| 300 SetRect(&_originalHwndRect, 0, 0, 0, 0); |
| 301 } |
| 302 |
| 303 VideoRenderDirect3D9::~VideoRenderDirect3D9() |
| 304 { |
| 305 //NOTE: we should not enter CriticalSection in here! |
| 306 |
| 307 // Signal event to exit thread, then delete it |
| 308 rtc::PlatformThread* tmpPtr = _screenUpdateThread.release(); |
| 309 if (tmpPtr) |
| 310 { |
| 311 _screenUpdateEvent->Set(); |
| 312 _screenUpdateEvent->StopTimer(); |
| 313 |
| 314 tmpPtr->Stop(); |
| 315 delete tmpPtr; |
| 316 } |
| 317 delete _screenUpdateEvent; |
| 318 |
| 319 //close d3d device |
| 320 CloseDevice(); |
| 321 |
| 322 // Delete all channels |
| 323 std::map<int, D3D9Channel*>::iterator it = _d3dChannels.begin(); |
| 324 while (it != _d3dChannels.end()) |
| 325 { |
| 326 delete it->second; |
| 327 it = _d3dChannels.erase(it); |
| 328 } |
| 329 // Clean the zOrder map |
| 330 _d3dZorder.clear(); |
| 331 |
| 332 if (_fullScreen) |
| 333 { |
| 334 // restore hwnd to original size and position |
| 335 ::SetWindowPos(_hWnd, HWND_NOTOPMOST, _originalHwndRect.left, |
| 336 _originalHwndRect.top, _originalHwndRect.right |
| 337 - _originalHwndRect.left, |
| 338 _originalHwndRect.bottom - _originalHwndRect.top, |
| 339 SWP_FRAMECHANGED); |
| 340 ::RedrawWindow(_hWnd, NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW |
| 341 | RDW_ERASE); |
| 342 ::RedrawWindow(NULL, NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW |
| 343 | RDW_ERASE); |
| 344 } |
| 345 |
| 346 delete &_refD3DCritsect; |
| 347 } |
| 348 |
| 349 DWORD VideoRenderDirect3D9::GetVertexProcessingCaps() |
| 350 { |
| 351 D3DCAPS9 caps; |
| 352 DWORD dwVertexProcessing = D3DCREATE_SOFTWARE_VERTEXPROCESSING; |
| 353 if (SUCCEEDED(_pD3D->GetDeviceCaps(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, |
| 354 &caps))) |
| 355 { |
| 356 if ((caps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT) |
| 357 == D3DDEVCAPS_HWTRANSFORMANDLIGHT) |
| 358 { |
| 359 dwVertexProcessing = D3DCREATE_HARDWARE_VERTEXPROCESSING; |
| 360 } |
| 361 } |
| 362 return dwVertexProcessing; |
| 363 } |
| 364 |
| 365 int VideoRenderDirect3D9::InitializeD3D(HWND hWnd, |
| 366 D3DPRESENT_PARAMETERS* pd3dpp) |
| 367 { |
| 368 // initialize Direct3D |
| 369 if (NULL == (_pD3D = Direct3DCreate9(D3D_SDK_VERSION))) |
| 370 { |
| 371 return -1; |
| 372 } |
| 373 |
| 374 // determine what type of vertex processing to use based on the device capab
ilities |
| 375 DWORD dwVertexProcessing = GetVertexProcessingCaps(); |
| 376 |
| 377 // get the display mode |
| 378 D3DDISPLAYMODE d3ddm; |
| 379 _pD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm); |
| 380 pd3dpp->BackBufferFormat = d3ddm.Format; |
| 381 |
| 382 // create the D3D device |
| 383 if (FAILED(_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, |
| 384 dwVertexProcessing | D3DCREATE_MULTITHREADED |
| 385 | D3DCREATE_FPU_PRESERVE, pd3dpp, |
| 386 &_pd3dDevice))) |
| 387 { |
| 388 //try the ref device |
| 389 if (FAILED(_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_REF, |
| 390 hWnd, dwVertexProcessing |
| 391 | D3DCREATE_MULTITHREADED |
| 392 | D3DCREATE_FPU_PRESERVE, |
| 393 pd3dpp, &_pd3dDevice))) |
| 394 { |
| 395 return -1; |
| 396 } |
| 397 } |
| 398 |
| 399 return 0; |
| 400 } |
| 401 |
| 402 int VideoRenderDirect3D9::ResetDevice() |
| 403 { |
| 404 WEBRTC_TRACE(kTraceInfo, kTraceVideo, -1, |
| 405 "VideoRenderDirect3D9::ResetDevice"); |
| 406 |
| 407 CriticalSectionScoped cs(&_refD3DCritsect); |
| 408 |
| 409 //release the channel texture |
| 410 std::map<int, D3D9Channel*>::iterator it; |
| 411 it = _d3dChannels.begin(); |
| 412 while (it != _d3dChannels.end()) |
| 413 { |
| 414 if (it->second) |
| 415 { |
| 416 it->second->ReleaseTexture(); |
| 417 } |
| 418 it++; |
| 419 } |
| 420 |
| 421 //close d3d device |
| 422 if (CloseDevice() != 0) |
| 423 { |
| 424 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 425 "VideoRenderDirect3D9::ResetDevice failed to CloseDevice"); |
| 426 return -1; |
| 427 } |
| 428 |
| 429 //reinit d3d device |
| 430 if (InitDevice() != 0) |
| 431 { |
| 432 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 433 "VideoRenderDirect3D9::ResetDevice failed to InitDevice"); |
| 434 return -1; |
| 435 } |
| 436 |
| 437 //recreate channel texture |
| 438 it = _d3dChannels.begin(); |
| 439 while (it != _d3dChannels.end()) |
| 440 { |
| 441 if (it->second) |
| 442 { |
| 443 it->second->RecreateTexture(_pd3dDevice); |
| 444 } |
| 445 it++; |
| 446 } |
| 447 |
| 448 return 0; |
| 449 } |
| 450 |
| 451 int VideoRenderDirect3D9::InitDevice() |
| 452 { |
| 453 // Set up the structure used to create the D3DDevice |
| 454 ZeroMemory(&_d3dpp, sizeof(_d3dpp)); |
| 455 _d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; |
| 456 _d3dpp.BackBufferFormat = D3DFMT_A8R8G8B8; |
| 457 if (GetWindowRect(_hWnd, &_originalHwndRect) == 0) |
| 458 { |
| 459 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 460 "VideoRenderDirect3D9::InitDevice Could not get window size
"); |
| 461 return -1; |
| 462 } |
| 463 if (!_fullScreen) |
| 464 { |
| 465 _winWidth = _originalHwndRect.right - _originalHwndRect.left; |
| 466 _winHeight = _originalHwndRect.bottom - _originalHwndRect.top; |
| 467 _d3dpp.Windowed = TRUE; |
| 468 _d3dpp.BackBufferHeight = 0; |
| 469 _d3dpp.BackBufferWidth = 0; |
| 470 } |
| 471 else |
| 472 { |
| 473 _winWidth = (LONG) ::GetSystemMetrics(SM_CXSCREEN); |
| 474 _winHeight = (LONG) ::GetSystemMetrics(SM_CYSCREEN); |
| 475 _d3dpp.Windowed = FALSE; |
| 476 _d3dpp.BackBufferWidth = _winWidth; |
| 477 _d3dpp.BackBufferHeight = _winHeight; |
| 478 _d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; |
| 479 } |
| 480 |
| 481 if (InitializeD3D(_hWnd, &_d3dpp) == -1) |
| 482 { |
| 483 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 484 "VideoRenderDirect3D9::InitDevice failed in InitializeD3D")
; |
| 485 return -1; |
| 486 } |
| 487 |
| 488 // Turn off culling, so we see the front and back of the triangle |
| 489 _pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); |
| 490 |
| 491 // Turn off D3D lighting, since we are providing our own vertex colors |
| 492 _pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE); |
| 493 |
| 494 // Settings for alpha blending |
| 495 _pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); |
| 496 _pd3dDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); |
| 497 _pd3dDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); |
| 498 |
| 499 _pd3dDevice->SetSamplerState( 0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR ); |
| 500 _pd3dDevice->SetSamplerState( 0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR ); |
| 501 _pd3dDevice->SetSamplerState( 0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR ); |
| 502 |
| 503 // Initialize Vertices |
| 504 CUSTOMVERTEX Vertices[] = { |
| 505 //front |
| 506 { -1.0f, -1.0f, 0.0f, 0xffffffff, 0, 1 }, { -1.0f, 1.0f, 0.0f, |
| 507 0xffffffff, 0, 0 }, |
| 508 { 1.0f, -1.0f, 0.0f, 0xffffffff, 1, 1 }, { 1.0f, 1.0f, 0.0f, |
| 509 0xffffffff, 1, 0 } }; |
| 510 |
| 511 // Create the vertex buffer. |
| 512 if (FAILED(_pd3dDevice->CreateVertexBuffer(sizeof(Vertices), 0, |
| 513 D3DFVF_CUSTOMVERTEX, |
| 514 D3DPOOL_DEFAULT, &_pVB, NULL ))) |
| 515 { |
| 516 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 517 "Failed to create the vertex buffer."); |
| 518 return -1; |
| 519 } |
| 520 |
| 521 // Now we fill the vertex buffer. |
| 522 VOID* pVertices; |
| 523 if (FAILED(_pVB->Lock(0, sizeof(Vertices), (void**) &pVertices, 0))) |
| 524 { |
| 525 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 526 "Failed to lock the vertex buffer."); |
| 527 return -1; |
| 528 } |
| 529 memcpy(pVertices, Vertices, sizeof(Vertices)); |
| 530 _pVB->Unlock(); |
| 531 |
| 532 return 0; |
| 533 } |
| 534 |
| 535 int32_t VideoRenderDirect3D9::Init() |
| 536 { |
| 537 WEBRTC_TRACE(kTraceInfo, kTraceVideo, -1, |
| 538 "VideoRenderDirect3D9::Init"); |
| 539 |
| 540 CriticalSectionScoped cs(&_refD3DCritsect); |
| 541 |
| 542 // Start rendering thread... |
| 543 if (!_screenUpdateThread) |
| 544 { |
| 545 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, "Thread not created"); |
| 546 return -1; |
| 547 } |
| 548 _screenUpdateThread->Start(); |
| 549 _screenUpdateThread->SetPriority(rtc::kRealtimePriority); |
| 550 |
| 551 // Start the event triggering the render process |
| 552 unsigned int monitorFreq = 60; |
| 553 DEVMODE dm; |
| 554 // initialize the DEVMODE structure |
| 555 ZeroMemory(&dm, sizeof(dm)); |
| 556 dm.dmSize = sizeof(dm); |
| 557 if (0 != EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &dm)) |
| 558 { |
| 559 monitorFreq = dm.dmDisplayFrequency; |
| 560 } |
| 561 _screenUpdateEvent->StartTimer(true, 1000 / monitorFreq); |
| 562 |
| 563 return InitDevice(); |
| 564 } |
| 565 |
| 566 int32_t VideoRenderDirect3D9::ChangeWindow(void* window) |
| 567 { |
| 568 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, "Not supported."); |
| 569 return -1; |
| 570 } |
| 571 |
| 572 int VideoRenderDirect3D9::UpdateRenderSurface() |
| 573 { |
| 574 CriticalSectionScoped cs(&_refD3DCritsect); |
| 575 |
| 576 // Check if there are any updated buffers |
| 577 bool updated = false; |
| 578 std::map<int, D3D9Channel*>::iterator it; |
| 579 it = _d3dChannels.begin(); |
| 580 while (it != _d3dChannels.end()) |
| 581 { |
| 582 |
| 583 D3D9Channel* channel = it->second; |
| 584 channel->IsUpdated(updated); |
| 585 if (updated) |
| 586 { |
| 587 break; |
| 588 } |
| 589 it++; |
| 590 } |
| 591 //nothing is updated, continue |
| 592 if (!updated) |
| 593 return -1; |
| 594 |
| 595 // Clear the backbuffer to a black color |
| 596 _pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, |
| 597 0); |
| 598 |
| 599 // Begin the scene |
| 600 if (SUCCEEDED(_pd3dDevice->BeginScene())) |
| 601 { |
| 602 _pd3dDevice->SetStreamSource(0, _pVB, 0, sizeof(CUSTOMVERTEX)); |
| 603 _pd3dDevice->SetFVF(D3DFVF_CUSTOMVERTEX); |
| 604 |
| 605 //draw all the channels |
| 606 //get texture from the channels |
| 607 LPDIRECT3DTEXTURE9 textureFromChannel = NULL; |
| 608 DWORD textureWidth, textureHeight; |
| 609 |
| 610 std::multimap<int, unsigned int>::reverse_iterator it; |
| 611 it = _d3dZorder.rbegin(); |
| 612 while (it != _d3dZorder.rend()) |
| 613 { |
| 614 // loop through all channels and streams in Z order |
| 615 int channel = it->second & 0x0000ffff; |
| 616 |
| 617 std::map<int, D3D9Channel*>::iterator ddIt; |
| 618 ddIt = _d3dChannels.find(channel); |
| 619 if (ddIt != _d3dChannels.end()) |
| 620 { |
| 621 // found the channel |
| 622 D3D9Channel* channelObj = ddIt->second; |
| 623 if (channelObj) |
| 624 { |
| 625 textureFromChannel = channelObj->GetTexture(); |
| 626 textureWidth = channelObj->GetTextureWidth(); |
| 627 textureHeight = channelObj->GetTextureHeight(); |
| 628 |
| 629 uint32_t zOrder; |
| 630 float startWidth, startHeight, stopWidth, stopHeight; |
| 631 channelObj->GetStreamSettings(0, zOrder, startWidth, |
| 632 startHeight, stopWidth, |
| 633 stopHeight); |
| 634 |
| 635 //draw the video stream |
| 636 UpdateVerticeBuffer(_pVB, 0, startWidth, startHeight, |
| 637 stopWidth, stopHeight); |
| 638 _pd3dDevice->SetTexture(0, textureFromChannel); |
| 639 _pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); |
| 640 |
| 641 //Notice channel that this frame as been rendered |
| 642 channelObj->RenderOffFrame(); |
| 643 } |
| 644 } |
| 645 it++; |
| 646 } |
| 647 |
| 648 //draw the logo |
| 649 if (_pTextureLogo) |
| 650 { |
| 651 UpdateVerticeBuffer(_pVB, 0, _logoLeft, _logoTop, _logoRight, |
| 652 _logoBottom); |
| 653 _pd3dDevice->SetTexture(0, _pTextureLogo); |
| 654 _pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); |
| 655 } |
| 656 |
| 657 // End the scene |
| 658 _pd3dDevice->EndScene(); |
| 659 } |
| 660 |
| 661 // Present the backbuffer contents to the display |
| 662 _pd3dDevice->Present(NULL, NULL, NULL, NULL ); |
| 663 |
| 664 return 0; |
| 665 } |
| 666 |
| 667 //set the alpha value of the pixal with a particular colorkey as 0 |
| 668 int VideoRenderDirect3D9::SetTransparentColor(LPDIRECT3DTEXTURE9 pTexture, |
| 669 DDCOLORKEY* transparentColorKe
y, |
| 670 DWORD width, |
| 671 DWORD height) |
| 672 { |
| 673 D3DLOCKED_RECT lr; |
| 674 if (!pTexture) |
| 675 return -1; |
| 676 |
| 677 CriticalSectionScoped cs(&_refD3DCritsect); |
| 678 if (SUCCEEDED(pTexture->LockRect(0, &lr, NULL, D3DLOCK_DISCARD))) |
| 679 { |
| 680 for (DWORD y = 0; y < height; y++) |
| 681 { |
| 682 DWORD dwOffset = y * width; |
| 683 |
| 684 for (DWORD x = 0; x < width; x) |
| 685 { |
| 686 DWORD temp = ((DWORD*) lr.pBits)[dwOffset + x]; |
| 687 if ((temp & 0x00FFFFFF) |
| 688 == transparentColorKey->dwColorSpaceLowValue) |
| 689 { |
| 690 temp &= 0x00FFFFFF; |
| 691 } |
| 692 else |
| 693 { |
| 694 temp |= 0xFF000000; |
| 695 } |
| 696 ((DWORD*) lr.pBits)[dwOffset + x] = temp; |
| 697 x++; |
| 698 } |
| 699 } |
| 700 pTexture->UnlockRect(0); |
| 701 return 0; |
| 702 } |
| 703 return -1; |
| 704 } |
| 705 |
| 706 /* |
| 707 * |
| 708 * Rendering process |
| 709 * |
| 710 */ |
| 711 bool VideoRenderDirect3D9::ScreenUpdateThreadProc(void* obj) |
| 712 { |
| 713 return static_cast<VideoRenderDirect3D9*> (obj)->ScreenUpdateProcess(); |
| 714 } |
| 715 |
| 716 bool VideoRenderDirect3D9::ScreenUpdateProcess() |
| 717 { |
| 718 _screenUpdateEvent->Wait(100); |
| 719 |
| 720 if (!_screenUpdateThread) |
| 721 { |
| 722 //stop the thread |
| 723 return false; |
| 724 } |
| 725 if (!_pd3dDevice) |
| 726 { |
| 727 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 728 "d3dDevice not created."); |
| 729 return true; |
| 730 } |
| 731 |
| 732 HRESULT hr = _pd3dDevice->TestCooperativeLevel(); |
| 733 |
| 734 if (SUCCEEDED(hr)) |
| 735 { |
| 736 UpdateRenderSurface(); |
| 737 } |
| 738 |
| 739 if (hr == D3DERR_DEVICELOST) |
| 740 { |
| 741 //Device is lost and cannot be reset yet |
| 742 |
| 743 } |
| 744 else if (hr == D3DERR_DEVICENOTRESET) |
| 745 { |
| 746 //Lost but we can reset it now |
| 747 //Note: the standard way is to call Reset, however for some reason doesn
't work here. |
| 748 //so we will release the device and create it again. |
| 749 ResetDevice(); |
| 750 } |
| 751 |
| 752 return true; |
| 753 } |
| 754 |
| 755 int VideoRenderDirect3D9::CloseDevice() |
| 756 { |
| 757 CriticalSectionScoped cs(&_refD3DCritsect); |
| 758 WEBRTC_TRACE(kTraceInfo, kTraceVideo, -1, |
| 759 "VideoRenderDirect3D9::CloseDevice"); |
| 760 |
| 761 if (_pTextureLogo != NULL) |
| 762 { |
| 763 _pTextureLogo->Release(); |
| 764 _pTextureLogo = NULL; |
| 765 } |
| 766 |
| 767 if (_pVB != NULL) |
| 768 { |
| 769 _pVB->Release(); |
| 770 _pVB = NULL; |
| 771 } |
| 772 |
| 773 if (_pd3dDevice != NULL) |
| 774 { |
| 775 _pd3dDevice->Release(); |
| 776 _pd3dDevice = NULL; |
| 777 } |
| 778 |
| 779 if (_pD3D != NULL) |
| 780 { |
| 781 _pD3D->Release(); |
| 782 _pD3D = NULL; |
| 783 } |
| 784 |
| 785 if (_pd3dSurface != NULL) |
| 786 _pd3dSurface->Release(); |
| 787 return 0; |
| 788 } |
| 789 |
| 790 D3D9Channel* VideoRenderDirect3D9::GetD3DChannel(int channel) |
| 791 { |
| 792 std::map<int, D3D9Channel*>::iterator ddIt; |
| 793 ddIt = _d3dChannels.find(channel & 0x0000ffff); |
| 794 D3D9Channel* ddobj = NULL; |
| 795 if (ddIt != _d3dChannels.end()) |
| 796 { |
| 797 ddobj = ddIt->second; |
| 798 } |
| 799 if (ddobj == NULL) |
| 800 { |
| 801 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 802 "Direct3D render failed to find channel"); |
| 803 return NULL; |
| 804 } |
| 805 return ddobj; |
| 806 } |
| 807 |
| 808 int32_t VideoRenderDirect3D9::DeleteChannel(const uint32_t streamId) |
| 809 { |
| 810 CriticalSectionScoped cs(&_refD3DCritsect); |
| 811 |
| 812 |
| 813 std::multimap<int, unsigned int>::iterator it; |
| 814 it = _d3dZorder.begin(); |
| 815 while (it != _d3dZorder.end()) |
| 816 { |
| 817 if ((streamId & 0x0000ffff) == (it->second & 0x0000ffff)) |
| 818 { |
| 819 it = _d3dZorder.erase(it); |
| 820 break; |
| 821 } |
| 822 it++; |
| 823 } |
| 824 |
| 825 std::map<int, D3D9Channel*>::iterator ddIt; |
| 826 ddIt = _d3dChannels.find(streamId & 0x0000ffff); |
| 827 if (ddIt != _d3dChannels.end()) |
| 828 { |
| 829 delete ddIt->second; |
| 830 _d3dChannels.erase(ddIt); |
| 831 return 0; |
| 832 } |
| 833 return -1; |
| 834 } |
| 835 |
| 836 VideoRenderCallback* VideoRenderDirect3D9::CreateChannel(const uint32_t channel, |
| 837 const uint32_t
zOrder, |
| 838 const float lef
t, |
| 839 const float top
, |
| 840 const float rig
ht, |
| 841 const float bot
tom) |
| 842 { |
| 843 CriticalSectionScoped cs(&_refD3DCritsect); |
| 844 |
| 845 //FIXME this should be done in VideoAPIWindows? stop the frame deliver first |
| 846 //remove the old channel |
| 847 DeleteChannel(channel); |
| 848 |
| 849 D3D9Channel* d3dChannel = new D3D9Channel(_pd3dDevice, |
| 850 &_refD3DCritsect, _trace); |
| 851 d3dChannel->SetStreamSettings(0, zOrder, left, top, right, bottom); |
| 852 |
| 853 // store channel |
| 854 _d3dChannels[channel & 0x0000ffff] = d3dChannel; |
| 855 |
| 856 // store Z order |
| 857 // default streamID is 0 |
| 858 _d3dZorder.insert( |
| 859 std::pair<int, unsigned int>(zOrder, channel & 0x0000ffff)
); |
| 860 |
| 861 return d3dChannel; |
| 862 } |
| 863 |
| 864 int32_t VideoRenderDirect3D9::GetStreamSettings(const uint32_t channel, |
| 865 const uint16_t streamId, |
| 866 uint32_t& zOrder, |
| 867 float& left, float& top, |
| 868 float& right, float& bottom) |
| 869 { |
| 870 std::map<int, D3D9Channel*>::iterator ddIt; |
| 871 ddIt = _d3dChannels.find(channel & 0x0000ffff); |
| 872 D3D9Channel* ddobj = NULL; |
| 873 if (ddIt != _d3dChannels.end()) |
| 874 { |
| 875 ddobj = ddIt->second; |
| 876 } |
| 877 if (ddobj == NULL) |
| 878 { |
| 879 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 880 "Direct3D render failed to find channel"); |
| 881 return -1; |
| 882 } |
| 883 // Only allow one stream per channel, demuxing is |
| 884 return ddobj->GetStreamSettings(0, zOrder, left, top, right, bottom); |
| 885 } |
| 886 |
| 887 int VideoRenderDirect3D9::UpdateVerticeBuffer(LPDIRECT3DVERTEXBUFFER9 pVB, |
| 888 int offset, |
| 889 float startWidth, |
| 890 float startHeight, |
| 891 float stopWidth, |
| 892 float stopHeight) |
| 893 { |
| 894 if (pVB == NULL) |
| 895 return -1; |
| 896 |
| 897 float left, right, top, bottom; |
| 898 |
| 899 //update the vertice buffer |
| 900 //0,1 => -1,1 |
| 901 left = startWidth * 2 - 1; |
| 902 right = stopWidth * 2 - 1; |
| 903 |
| 904 //0,1 => 1,-1 |
| 905 top = 1 - startHeight * 2; |
| 906 bottom = 1 - stopHeight * 2; |
| 907 |
| 908 CUSTOMVERTEX newVertices[] = { |
| 909 //logo |
| 910 { left, bottom, 0.0f, 0xffffffff, 0, 1 }, { left, top, 0.0f, |
| 911 0xffffffff, 0, 0 }, |
| 912 { right, bottom, 0.0f, 0xffffffff, 1, 1 }, { right, top, 0.0f, |
| 913 0xffffffff, 1, 0 }, }; |
| 914 // Now we fill the vertex buffer. |
| 915 VOID* pVertices; |
| 916 if (FAILED(pVB->Lock(sizeof(CUSTOMVERTEX) * offset, sizeof(newVertices), |
| 917 (void**) &pVertices, 0))) |
| 918 { |
| 919 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 920 "Failed to lock the vertex buffer."); |
| 921 return -1; |
| 922 } |
| 923 memcpy(pVertices, newVertices, sizeof(newVertices)); |
| 924 pVB->Unlock(); |
| 925 |
| 926 return 0; |
| 927 } |
| 928 |
| 929 int32_t VideoRenderDirect3D9::StartRender() |
| 930 { |
| 931 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, "Not supported."); |
| 932 return 0; |
| 933 } |
| 934 |
| 935 int32_t VideoRenderDirect3D9::StopRender() |
| 936 { |
| 937 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, "Not supported."); |
| 938 return 0; |
| 939 } |
| 940 |
| 941 bool VideoRenderDirect3D9::IsFullScreen() |
| 942 { |
| 943 return _fullScreen; |
| 944 } |
| 945 |
| 946 int32_t VideoRenderDirect3D9::SetCropping(const uint32_t channel, |
| 947 const uint16_t streamId, |
| 948 const float left, const float top, |
| 949 const float right, const float bottom) |
| 950 { |
| 951 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, "Not supported."); |
| 952 return 0; |
| 953 } |
| 954 |
| 955 int32_t VideoRenderDirect3D9::SetTransparentBackground( |
| 956 const bool enab
le) |
| 957 { |
| 958 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, "Not supported."); |
| 959 return 0; |
| 960 } |
| 961 |
| 962 int32_t VideoRenderDirect3D9::SetText(const uint8_t textId, |
| 963 const uint8_t* text, |
| 964 const int32_t textLength, |
| 965 const uint32_t colorText, |
| 966 const uint32_t colorBg, |
| 967 const float left, const float top, |
| 968 const float rigth, const float bottom) |
| 969 { |
| 970 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, "Not supported."); |
| 971 return 0; |
| 972 } |
| 973 |
| 974 int32_t VideoRenderDirect3D9::SetBitmap(const void* bitMap, |
| 975 const uint8_t pictureId, |
| 976 const void* colorKey, |
| 977 const float left, const float top, |
| 978 const float right, const float bottom) |
| 979 { |
| 980 if (!bitMap) |
| 981 { |
| 982 if (_pTextureLogo != NULL) |
| 983 { |
| 984 _pTextureLogo->Release(); |
| 985 _pTextureLogo = NULL; |
| 986 } |
| 987 WEBRTC_TRACE(kTraceInfo, kTraceVideo, -1, "Remove bitmap."); |
| 988 return 0; |
| 989 } |
| 990 |
| 991 // sanity |
| 992 if (left > 1.0f || left < 0.0f || |
| 993 top > 1.0f || top < 0.0f || |
| 994 right > 1.0f || right < 0.0f || |
| 995 bottom > 1.0f || bottom < 0.0f) |
| 996 { |
| 997 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 998 "Direct3D SetBitmap invalid parameter"); |
| 999 return -1; |
| 1000 } |
| 1001 |
| 1002 if ((bottom <= top) || (right <= left)) |
| 1003 { |
| 1004 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 1005 "Direct3D SetBitmap invalid parameter"); |
| 1006 return -1; |
| 1007 } |
| 1008 |
| 1009 CriticalSectionScoped cs(&_refD3DCritsect); |
| 1010 |
| 1011 unsigned char* srcPtr; |
| 1012 HGDIOBJ oldhand; |
| 1013 BITMAPINFO pbi; |
| 1014 BITMAP bmap; |
| 1015 HDC hdcNew; |
| 1016 hdcNew = CreateCompatibleDC(0); |
| 1017 // Fill out the BITMAP structure. |
| 1018 GetObject((HBITMAP)bitMap, sizeof(bmap), &bmap); |
| 1019 //Select the bitmap handle into the new device context. |
| 1020 oldhand = SelectObject(hdcNew, (HGDIOBJ) bitMap); |
| 1021 // we are done with this object |
| 1022 DeleteObject(oldhand); |
| 1023 pbi.bmiHeader.biSize = 40; |
| 1024 pbi.bmiHeader.biWidth = bmap.bmWidth; |
| 1025 pbi.bmiHeader.biHeight = bmap.bmHeight; |
| 1026 pbi.bmiHeader.biPlanes = 1; |
| 1027 pbi.bmiHeader.biBitCount = bmap.bmBitsPixel; |
| 1028 pbi.bmiHeader.biCompression = BI_RGB; |
| 1029 pbi.bmiHeader.biSizeImage = bmap.bmWidth * bmap.bmHeight * 3; |
| 1030 srcPtr = new unsigned char[bmap.bmWidth * bmap.bmHeight * 4]; |
| 1031 // the original un-stretched image in RGB24 |
| 1032 int pixelHeight = GetDIBits(hdcNew, (HBITMAP)bitMap, 0, bmap.bmHeight, srcPt
r, &pbi, |
| 1033 DIB_RGB_COLORS); |
| 1034 if (pixelHeight == 0) |
| 1035 { |
| 1036 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 1037 "Direct3D failed to GetDIBits in SetBitmap"); |
| 1038 delete[] srcPtr; |
| 1039 return -1; |
| 1040 } |
| 1041 DeleteDC(hdcNew); |
| 1042 if (pbi.bmiHeader.biBitCount != 24 && pbi.bmiHeader.biBitCount != 32) |
| 1043 { |
| 1044 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 1045 "Direct3D failed to SetBitmap invalid bit depth"); |
| 1046 delete[] srcPtr; |
| 1047 return -1; |
| 1048 } |
| 1049 |
| 1050 HRESULT ret; |
| 1051 //release the previous logo texture |
| 1052 if (_pTextureLogo != NULL) |
| 1053 { |
| 1054 _pTextureLogo->Release(); |
| 1055 _pTextureLogo = NULL; |
| 1056 } |
| 1057 ret = _pd3dDevice->CreateTexture(bmap.bmWidth, bmap.bmHeight, 1, 0, |
| 1058 D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, |
| 1059 &_pTextureLogo, NULL); |
| 1060 if (FAILED(ret)) |
| 1061 { |
| 1062 _pTextureLogo = NULL; |
| 1063 delete[] srcPtr; |
| 1064 return -1; |
| 1065 } |
| 1066 if (!_pTextureLogo) |
| 1067 { |
| 1068 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 1069 "Texture for rendering not initialized."); |
| 1070 delete[] srcPtr; |
| 1071 return -1; |
| 1072 } |
| 1073 |
| 1074 D3DLOCKED_RECT lr; |
| 1075 if (FAILED(_pTextureLogo->LockRect(0, &lr, NULL, 0))) |
| 1076 { |
| 1077 delete[] srcPtr; |
| 1078 return -1; |
| 1079 } |
| 1080 unsigned char* dstPtr = (UCHAR*) lr.pBits; |
| 1081 int pitch = bmap.bmWidth * 4; |
| 1082 |
| 1083 if (pbi.bmiHeader.biBitCount == 24) |
| 1084 { |
| 1085 ConvertRGB24ToARGB(srcPtr, dstPtr, bmap.bmWidth, bmap.bmHeight, 0); |
| 1086 } |
| 1087 else |
| 1088 { |
| 1089 unsigned char* srcTmp = srcPtr + (bmap.bmWidth * 4) * (bmap.bmHeight - 1
); |
| 1090 for (int i = 0; i < bmap.bmHeight; ++i) |
| 1091 { |
| 1092 memcpy(dstPtr, srcTmp, bmap.bmWidth * 4); |
| 1093 srcTmp -= bmap.bmWidth * 4; |
| 1094 dstPtr += pitch; |
| 1095 } |
| 1096 } |
| 1097 |
| 1098 delete[] srcPtr; |
| 1099 if (FAILED(_pTextureLogo->UnlockRect(0))) |
| 1100 { |
| 1101 return -1; |
| 1102 } |
| 1103 |
| 1104 if (colorKey) |
| 1105 { |
| 1106 DDCOLORKEY* ddColorKey = |
| 1107 static_cast<DDCOLORKEY*> (const_cast<void*> (colorKey)); |
| 1108 SetTransparentColor(_pTextureLogo, ddColorKey, bmap.bmWidth, |
| 1109 bmap.bmHeight); |
| 1110 } |
| 1111 |
| 1112 //update the vertice buffer |
| 1113 //0,1 => -1,1 |
| 1114 _logoLeft = left; |
| 1115 _logoRight = right; |
| 1116 |
| 1117 //0,1 => 1,-1 |
| 1118 _logoTop = top; |
| 1119 _logoBottom = bottom; |
| 1120 |
| 1121 return 0; |
| 1122 |
| 1123 } |
| 1124 |
| 1125 int32_t VideoRenderDirect3D9::GetGraphicsMemory(uint64_t& totalMemory, |
| 1126 uint64_t& availableMemory) |
| 1127 { |
| 1128 totalMemory = _totalMemory; |
| 1129 availableMemory = _availableMemory; |
| 1130 return 0; |
| 1131 } |
| 1132 |
| 1133 int32_t VideoRenderDirect3D9::ConfigureRenderer(const uint32_t channel, |
| 1134 const uint16_t streamId, |
| 1135 const unsigned int zOrder, |
| 1136 const float left, |
| 1137 const float top, |
| 1138 const float right, |
| 1139 const float bottom) |
| 1140 { |
| 1141 std::map<int, D3D9Channel*>::iterator ddIt; |
| 1142 ddIt = _d3dChannels.find(channel & 0x0000ffff); |
| 1143 D3D9Channel* ddobj = NULL; |
| 1144 if (ddIt != _d3dChannels.end()) |
| 1145 { |
| 1146 ddobj = ddIt->second; |
| 1147 } |
| 1148 if (ddobj == NULL) |
| 1149 { |
| 1150 WEBRTC_TRACE(kTraceError, kTraceVideo, -1, |
| 1151 "Direct3D render failed to find channel"); |
| 1152 return -1; |
| 1153 } |
| 1154 // Only allow one stream per channel, demuxing is |
| 1155 ddobj->SetStreamSettings(0, zOrder, left, top, right, bottom); |
| 1156 |
| 1157 return 0; |
| 1158 } |
| 1159 |
| 1160 } // namespace webrtc |
OLD | NEW |