From 3040671c5560c5caf09cc56a652d79793676eaff Mon Sep 17 00:00:00 2001 From: xwidghet Date: Sun, 19 Feb 2017 18:02:10 -0800 Subject: [PATCH] Fix predictive frame rendering to actually delay the next game loop until the right time. Shoutouts to C++11 sleep actually sleeping for the time I asked for. --- src/Actor.cpp | 2 +- src/ActorFrame.cpp | 3 -- src/ActorFrameTexture.cpp | 3 -- src/ActorMultiTexture.cpp | 3 -- src/ActorMultiVertex.cpp | 3 -- src/Background.cpp | 3 -- src/BeginnerHelper.cpp | 3 -- src/BitmapText.cpp | 5 +- src/DancingCharacters.cpp | 3 -- src/Model.cpp | 9 ---- src/NoteDisplay.cpp | 17 +------ src/NoteField.cpp | 3 -- src/Player.cpp | 3 -- src/RageDisplay.cpp | 104 +++++++++++++++++--------------------- src/RageDisplay.h | 3 -- src/RageDisplay_D3D.cpp | 16 ++---- src/RageDisplay_OGL.cpp | 51 +++++++++---------- src/ScreenManager.cpp | 19 +++---- src/Sprite.cpp | 6 --- 19 files changed, 85 insertions(+), 174 deletions(-) diff --git a/src/Actor.cpp b/src/Actor.cpp index 1fb7b0720d..a19011e14a 100644 --- a/src/Actor.cpp +++ b/src/Actor.cpp @@ -305,7 +305,7 @@ void Actor::Draw() { if( !m_bVisible || m_fHibernateSecondsLeft > 0 || - this->EarlyAbortDraw() || !DISPLAY->ShouldRenderFrame() ) + this->EarlyAbortDraw() ) { return; // early abort } diff --git a/src/ActorFrame.cpp b/src/ActorFrame.cpp index e19b76a763..dd165eac62 100644 --- a/src/ActorFrame.cpp +++ b/src/ActorFrame.cpp @@ -222,9 +222,6 @@ void ActorFrame::BeginDraw() void ActorFrame::DrawPrimitives() { - if (!DISPLAY->ShouldRenderFrame()) - return; - if( m_bClearZBuffer ) { LuaHelpers::ReportScriptErrorFmt( "ClearZBuffer not supported on ActorFrames" ); diff --git a/src/ActorFrameTexture.cpp b/src/ActorFrameTexture.cpp index 15cadb35e4..0943843561 100644 --- a/src/ActorFrameTexture.cpp +++ b/src/ActorFrameTexture.cpp @@ -74,9 +74,6 @@ void ActorFrameTexture::DrawPrimitives() if( m_pRenderTarget == NULL ) return; - if (!DISPLAY->ShouldRenderFrame()) - return; - m_pRenderTarget->BeginRenderingTo( m_bPreserveTexture ); ActorFrame::DrawPrimitives(); diff --git a/src/ActorMultiTexture.cpp b/src/ActorMultiTexture.cpp index de1f663916..01e4e2d958 100644 --- a/src/ActorMultiTexture.cpp +++ b/src/ActorMultiTexture.cpp @@ -89,9 +89,6 @@ void ActorMultiTexture::SetTextureMode( int iIndex, TextureMode tm ) void ActorMultiTexture::DrawPrimitives() { - if (!DISPLAY->ShouldRenderFrame()) - return; - Actor::SetGlobalRenderStates(); // set Actor-specified render states RectF quadVerticies; diff --git a/src/ActorMultiVertex.cpp b/src/ActorMultiVertex.cpp index 7605ec57e8..2e4af18d05 100644 --- a/src/ActorMultiVertex.cpp +++ b/src/ActorMultiVertex.cpp @@ -216,9 +216,6 @@ void ActorMultiVertex::SetVertexCoords( int index, float TexCoordX, float TexCoo void ActorMultiVertex::DrawPrimitives() { - if (!DISPLAY->ShouldRenderFrame()) - return; - Actor::SetGlobalRenderStates(); // set Actor-specified render states DISPLAY->ClearAllTextures(); diff --git a/src/Background.cpp b/src/Background.cpp index 216eacc0ad..3f6b4999e8 100644 --- a/src/Background.cpp +++ b/src/Background.cpp @@ -843,9 +843,6 @@ void BackgroundImpl::Update( float fDeltaTime ) void BackgroundImpl::DrawPrimitives() { - if (!DISPLAY->ShouldRenderFrame()) - return; - if( g_fBGBrightness == 0.0f ) return; diff --git a/src/BeginnerHelper.cpp b/src/BeginnerHelper.cpp index 0ce621e677..c4b23ba306 100644 --- a/src/BeginnerHelper.cpp +++ b/src/BeginnerHelper.cpp @@ -234,9 +234,6 @@ void BeginnerHelper::DrawPrimitives() if( !m_bInitialized ) return; - if (!DISPLAY->ShouldRenderFrame()) - return; - ActorFrame::DrawPrimitives(); m_sFlash.Draw(); diff --git a/src/BitmapText.cpp b/src/BitmapText.cpp index 49457f7b6f..2c4171fe19 100644 --- a/src/BitmapText.cpp +++ b/src/BitmapText.cpp @@ -349,7 +349,7 @@ void BitmapText::DrawChars( bool bUseStrokeTexture ) { // bail if cropped all the way if (m_pTempState->crop.left + m_pTempState->crop.right >= 1 || - m_pTempState->crop.top + m_pTempState->crop.bottom >= 1 || !DISPLAY->ShouldRenderFrame()) + m_pTempState->crop.top + m_pTempState->crop.bottom >= 1 ) { return; } @@ -725,9 +725,6 @@ bool BitmapText::EarlyAbortDraw() const // draw text at x, y using colorTop blended down to colorBottom, with size multiplied by scale void BitmapText::DrawPrimitives() { - if (!DISPLAY->ShouldRenderFrame()) - return; - Actor::SetGlobalRenderStates(); // set Actor-specified render states DISPLAY->SetTextureMode( TextureUnit_1, TextureMode_Modulate ); diff --git a/src/DancingCharacters.cpp b/src/DancingCharacters.cpp index 1d82911a7f..9c4c9bb4fe 100644 --- a/src/DancingCharacters.cpp +++ b/src/DancingCharacters.cpp @@ -310,9 +310,6 @@ void DancingCharacters::Change2DAnimState( PlayerNumber pn, int iState ) void DancingCharacters::DrawPrimitives() { - if (!DISPLAY->ShouldRenderFrame()) - return; - DISPLAY->CameraPushMatrix(); float fPercentIntoSweep; diff --git a/src/Model.cpp b/src/Model.cpp index 8794952e2d..befa97e157 100644 --- a/src/Model.cpp +++ b/src/Model.cpp @@ -293,9 +293,6 @@ bool Model::EarlyAbortDraw() const void Model::DrawCelShaded() { - if (!DISPLAY->ShouldRenderFrame()) - return; - // First pass: shell. We only want the backfaces for this. DISPLAY->SetCelShaded(1); DISPLAY->SetCullMode(CULL_FRONT); @@ -313,9 +310,6 @@ void Model::DrawCelShaded() void Model::DrawPrimitives() { - if (!DISPLAY->ShouldRenderFrame()) - return; - Actor::SetGlobalRenderStates(); // set Actor-specified render states // Don't if we're fully transparent @@ -471,9 +465,6 @@ void Model::DrawPrimitives() void Model::DrawMesh( int i ) const { - if (!DISPLAY->ShouldRenderFrame()) - return; - const msMesh *pMesh = &m_pGeometry->m_Meshes[i]; // apply mesh-specific bone (if any) diff --git a/src/NoteDisplay.cpp b/src/NoteDisplay.cpp index 702e52218a..facc6aa96e 100644 --- a/src/NoteDisplay.cpp +++ b/src/NoteDisplay.cpp @@ -448,9 +448,6 @@ bool NoteDisplay::DrawHoldsInRange(const NoteFieldRenderArgs& field_args, const NoteColumnRenderArgs& column_args, const vector& tap_set) { - if (!DISPLAY->ShouldRenderFrame()) - return false; - bool any_upcoming = false; for(vector::const_iterator tapit= tap_set.begin(); tapit != tap_set.end(); ++tapit) @@ -517,9 +514,6 @@ bool NoteDisplay::DrawTapsInRange(const NoteFieldRenderArgs& field_args, const NoteColumnRenderArgs& column_args, const vector& tap_set) { - if (!DISPLAY->ShouldRenderFrame()) - return false; - bool any_upcoming= false; // draw notes from furthest to closest for(vector::const_iterator tapit= @@ -706,10 +700,7 @@ struct StripBuffer } void Draw() { - if( DISPLAY->ShouldRenderFrame() ) - { - DISPLAY->DrawSymmetricQuadStrip(buf, v - buf); - } + DISPLAY->DrawSymmetricQuadStrip(buf, v - buf); } int Used() const { return v - buf; } int Free() const { return size - Used(); } @@ -1342,9 +1333,6 @@ void NoteDisplay::DrawTap(const TapNote& tn, bool bOnSameRowAsHoldStart, bool bOnSameRowAsRollStart, bool bIsAddition, float fPercentFadeToFail) { - if ( !DISPLAY->ShouldRenderFrame() ) - return; - Actor* pActor = NULL; NotePart part = NotePart_Tap; /* @@ -1491,9 +1479,6 @@ void NoteColumnRenderer::UpdateReceptorGhostStuff(Actor* receptor) const void NoteColumnRenderer::DrawPrimitives() { - if (!DISPLAY->ShouldRenderFrame()) - return; - m_column_render_args.song_beat= m_field_render_args->player_state->GetDisplayedPosition().m_fSongBeatVisible; m_column_render_args.pos_handler= &NCR_current.m_pos_handler; m_column_render_args.rot_handler= &NCR_current.m_rot_handler; diff --git a/src/NoteField.cpp b/src/NoteField.cpp index 9395a7859c..fb2196125a 100644 --- a/src/NoteField.cpp +++ b/src/NoteField.cpp @@ -706,9 +706,6 @@ void NoteField::CalcPixelsBeforeAndAfterTargets() void NoteField::DrawPrimitives() { - if (!DISPLAY->ShouldRenderFrame()) - return; - //LOG->Trace( "NoteField::DrawPrimitives()" ); // This should be filled in on the first update. diff --git a/src/Player.cpp b/src/Player.cpp index e508dd541b..bad8c4ed21 100644 --- a/src/Player.cpp +++ b/src/Player.cpp @@ -1521,9 +1521,6 @@ void Player::ApplyWaitingTransforms() void Player::DrawPrimitives() { - if (!DISPLAY->ShouldRenderFrame()) - return; - // TODO: Remove use of PlayerNumber. PlayerNumber pn = m_pPlayerState->m_PlayerNumber; diff --git a/src/RageDisplay.cpp b/src/RageDisplay.cpp index e7f08aea0e..22141a93c5 100644 --- a/src/RageDisplay.cpp +++ b/src/RageDisplay.cpp @@ -35,7 +35,6 @@ static int g_iFramesRenderedSinceLastCheck, g_iNumChecksSinceLastReset; static RageTimer g_LastFrameEndedAtRage( RageZeroTimer ); static auto g_LastFrameEndedAt = std::chrono::steady_clock::now(); -static auto g_FrameExecutionTime = std::chrono::steady_clock::now(); static auto g_FrameRenderTime = std::chrono::steady_clock::now(); static std::chrono::nanoseconds g_LastFrameRenderTime; static std::chrono::nanoseconds g_LastFramePresentTime; @@ -127,11 +126,8 @@ void RageDisplay::ProcessStatsOnFlip() { if (PREFSMAN->m_bShowStats || LOG_FPS) { - if (ShouldRenderFrame()) - { - g_iFramesRenderedSinceLastCheck++; - g_iFramesRenderedSinceLastReset++; - } + g_iFramesRenderedSinceLastCheck++; + g_iFramesRenderedSinceLastReset++; std::chrono::duration timeDelta = std::chrono::steady_clock::now() - g_LastCheckTimer; float checkTime = timeDelta.count(); @@ -937,7 +933,7 @@ void RageDisplay::DrawCircle( const RageSpriteVertex &v, float radius ) void RageDisplay::FrameLimitBeforeVsync() { - if ( presentFrame && g_fPredictiveFrameLimit.Get() ) + if ( g_fPredictiveFrameLimit.Get() ) { auto afterRender = std::chrono::steady_clock::now(); auto endTime = afterRender - g_FrameRenderTime; @@ -969,8 +965,8 @@ void RageDisplay::FrameLimitBeforeVsync() } // Frame pacing code -// The aim of this function is to make it so we only render frames the user can see -// And to render those frames as close to the display present time as possible to reduce display lag +// The aim of this function is to delay the start of the next loop as long as possible +// so that what the player sees when using VSync is as close to real time as possible. // // The issue: // If we wait too long we miss the present and cause a stutter that displays information which could be over 1 frame old @@ -985,63 +981,55 @@ void RageDisplay::FrameLimitAfterVsync( int iFPS ) else if ( !PREFSMAN->m_bVsync.Get() && g_fFrameLimit.Get() == 0 && g_fFrameLimitGameplay.Get() == 0 ) return; - if ( presentFrame ) + g_LastFrameEndedAt = std::chrono::steady_clock::now(); + + // Get the target frame time + double waitTime = 0.0; + if (SCREENMAN && SCREENMAN->GetTopScreen()) { - presentFrame = false; - g_LastFrameEndedAt = std::chrono::steady_clock::now(); - g_FrameExecutionTime = g_LastFrameEndedAt; + if (SCREENMAN->GetTopScreen()->GetScreenType() == gameplay && g_fFrameLimitGameplay.Get() > 0) + waitTime = 1.0 / g_fFrameLimitGameplay.Get(); + else if (SCREENMAN->GetTopScreen()->GetScreenType() != gameplay && g_fFrameLimit.Get() > 0) + waitTime = 1.0 / g_fFrameLimit.Get(); } - else - { - // Get the timings for the game logic loop, render loop, and present time - // Along with how long we are waiting for, e.g. Frame Limiting - auto loopEndTime = std::chrono::steady_clock::now() - g_FrameExecutionTime; - double waitTime = 0.0; - if (SCREENMAN && SCREENMAN->GetTopScreen()) - { - if (SCREENMAN->GetTopScreen()->GetScreenType() == gameplay && g_fFrameLimitGameplay.Get() > 0) - waitTime = 1.0 / g_fFrameLimitGameplay.Get(); - else if (SCREENMAN->GetTopScreen()->GetScreenType() != gameplay && g_fFrameLimit.Get() > 0) - waitTime = 1.0 / g_fFrameLimit.Get(); - } - - // Not using frame limiter and vsync is enabled - // Or Frame limiter is set beyond vsync - if (PREFSMAN->m_bVsync.Get() && (waitTime == 0.0 || waitTime < (1.0 / iFPS))) - waitTime = 1.0 / iFPS; - - auto waitTimeNano = std::chrono::duration(waitTime); - auto waitTimeActuallyNano = std::chrono::duration_cast(waitTimeNano); + // Not using frame limiter and vsync is enabled + // Or Frame limiter is set beyond vsync + if (PREFSMAN->m_bVsync.Get() && (waitTime == 0.0 || waitTime < (1.0 / iFPS))) + waitTime = 1.0 / iFPS; + + // Calculate wait time and attempt to not overshoot waiting + // Cautiousness is needed due to CPU power states and driver present times causing entire loop time variations + // of around +-250 microseconds. + + // Conservative default of 10% target frame time is used incase someone is using really old hardware + double waitCautiousness = g_fFrameLimitPercent.Get() > 0 ? g_fFrameLimitPercent.Get() : 0.90; - // Calculate wait time and attempt to not overshoot waiting - // - // Cannot have anything which takes a while to execute after the afterLoop time get as it won't be taken into account - double waitCautiousness = g_fFrameLimitPercent.Get() > 0 ? g_fFrameLimitPercent.Get() : 0.90; - auto renderTime = g_LastFrameRenderTime + g_LastFramePresentTime - loopEndTime; + // Target frame time + waitTime *= waitCautiousness; + auto waitTimeNano = std::chrono::duration(waitTime); + auto waitTimeActuallyNano = std::chrono::duration_cast(waitTimeNano); - auto afterLoop = std::chrono::steady_clock::now(); - auto timeTillRender = afterLoop - g_LastFrameEndedAt; + // Last render time, DirectX and OpenGL do work in present time so we need to include them + auto renderTime = g_LastFrameRenderTime + g_LastFramePresentTime; + + // ex. 8.33ms refresh rate - 1ms render time + auto startLoopTime = waitTimeActuallyNano - renderTime; - // Check if we have enough time to do another loop, or if that'll make us late - if (timeTillRender >= (waitCautiousness * (waitTimeActuallyNano - loopEndTime - renderTime))) - { - presentFrame = true; - g_FrameRenderTime = std::chrono::steady_clock::now(); - } + // Check if we need to wait + if (startLoopTime.count() < 0) + { + // Just go, we're late + //LOG->Trace("Game loop start is late by %d", startLoopTime); + } + else + { + //LOG->Trace("Waiting before game loop by %d", startLoopTime); + // Wait until our desired time, aka g_fFrameLimitPercent of the way until needed present time + std::this_thread::sleep_for(startLoopTime); } -} - -// Both ShouldRenderFrame and ShouldPresentFrame are the same, -// but they are here in-case in the future we find theres a reason to change how they are used -bool RageDisplay::ShouldRenderFrame() -{ - return presentFrame; -} -bool RageDisplay::ShouldPresentFrame() -{ - return presentFrame; + g_FrameRenderTime = std::chrono::steady_clock::now(); } void RageDisplay::SetPresentTime(std::chrono::nanoseconds presentTime) diff --git a/src/RageDisplay.h b/src/RageDisplay.h index 39f69f5a58..f8e6b39721 100644 --- a/src/RageDisplay.h +++ b/src/RageDisplay.h @@ -188,10 +188,7 @@ class RageDisplay virtual RString GetApiDescription() const = 0; virtual void GetDisplayResolutions( DisplayResolutions &out ) const = 0; - bool ShouldRenderFrame(); - bool ShouldPresentFrame(); void SetPresentTime(std::chrono::nanoseconds presentTime); - bool presentFrame = true; // Don't override this. Override TryVideoMode() instead. // This will set the video mode to be as close as possible to params. diff --git a/src/RageDisplay_D3D.cpp b/src/RageDisplay_D3D.cpp index 81ef4cce15..1383b4dd1f 100644 --- a/src/RageDisplay_D3D.cpp +++ b/src/RageDisplay_D3D.cpp @@ -586,11 +586,8 @@ bool RageDisplay_D3D::BeginFrame() } } - if (DISPLAY->ShouldRenderFrame()) - { - g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, + g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0x00000000); - } g_pd3dDevice->BeginScene(); @@ -603,14 +600,11 @@ void RageDisplay_D3D::EndFrame() FrameLimitBeforeVsync(); - if (ShouldPresentFrame()) - { - auto beforePresent = std::chrono::steady_clock::now(); - g_pd3dDevice->Present(0, 0, 0, 0); + auto beforePresent = std::chrono::steady_clock::now(); + g_pd3dDevice->Present(0, 0, 0, 0); - auto afterPresent = std::chrono::steady_clock::now(); - SetPresentTime(afterPresent - beforePresent); - } + auto afterPresent = std::chrono::steady_clock::now(); + SetPresentTime(afterPresent - beforePresent); FrameLimitAfterVsync( (*GetActualVideoModeParams()).rate ); diff --git a/src/RageDisplay_OGL.cpp b/src/RageDisplay_OGL.cpp index baaee8e7a1..c757d8ce2e 100644 --- a/src/RageDisplay_OGL.cpp +++ b/src/RageDisplay_OGL.cpp @@ -786,15 +786,12 @@ bool RageDisplay_Legacy::BeginFrame() /* We do this in here, rather than ResolutionChanged, or we won't update the * viewport for the concurrent rendering context. */ - if (!DISPLAY || DISPLAY->ShouldRenderFrame()) - { - int fWidth = (*g_pWind->GetActualVideoModeParams()).width; - int fHeight = (*g_pWind->GetActualVideoModeParams()).height; - glViewport(0, 0, fWidth, fHeight); - glClearColor(0, 0, 0, 0); - SetZWrite(true); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - } + int fWidth = (*g_pWind->GetActualVideoModeParams()).width; + int fHeight = (*g_pWind->GetActualVideoModeParams()).height; + glViewport(0, 0, fWidth, fHeight); + glClearColor(0, 0, 0, 0); + SetZWrite(true); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); return RageDisplay::BeginFrame(); } @@ -802,29 +799,27 @@ bool RageDisplay_Legacy::BeginFrame() void RageDisplay_Legacy::EndFrame() { FrameLimitBeforeVsync(); - if (ShouldPresentFrame()) - { - auto beforePresent = std::chrono::steady_clock::now(); - g_pWind->SwapBuffers(); + auto beforePresent = std::chrono::steady_clock::now(); + g_pWind->SwapBuffers(); - // Some would advise against glFinish(), ever. Those people don't realize - // the degree of freedom GL hosts are permitted in queueing commands. - // If left to its own devices, the host could lag behind several frames' worth - // of commands. - // glFlush() only forces the host to not wait to execute all commands - // sent so far; it does NOT block on those commands until they finish. - // glFinish() blocks. We WANT to block. Why? This puts the engine state - // reflected by the next frame as close as possible to the on-screen - // appearance of that frame. - glFinish(); + // Some would advise against glFinish(), ever. Those people don't realize + // the degree of freedom GL hosts are permitted in queueing commands. + // If left to its own devices, the host could lag behind several frames' worth + // of commands. + // glFlush() only forces the host to not wait to execute all commands + // sent so far; it does NOT block on those commands until they finish. + // glFinish() blocks. We WANT to block. Why? This puts the engine state + // reflected by the next frame as close as possible to the on-screen + // appearance of that frame. + glFinish(); - g_pWind->Update(); + g_pWind->Update(); - auto afterPresent = std::chrono::steady_clock::now(); - auto endTime = afterPresent - beforePresent; + auto afterPresent = std::chrono::steady_clock::now(); + auto endTime = afterPresent - beforePresent; + + SetPresentTime(endTime); - SetPresentTime(endTime); - } FrameLimitAfterVsync((*GetActualVideoModeParams()).rate); RageDisplay::EndFrame(); diff --git a/src/ScreenManager.cpp b/src/ScreenManager.cpp index 23642aa380..7d6df6dcde 100644 --- a/src/ScreenManager.cpp +++ b/src/ScreenManager.cpp @@ -504,19 +504,16 @@ void ScreenManager::Draw() if( !DISPLAY->BeginFrame() ) return; - if (DISPLAY->ShouldRenderFrame()) - { - DISPLAY->CameraPushMatrix(); - DISPLAY->LoadMenuPerspective(0, SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_CENTER_X, SCREEN_CENTER_Y); - g_pSharedBGA->Draw(); - DISPLAY->CameraPopMatrix(); + DISPLAY->CameraPushMatrix(); + DISPLAY->LoadMenuPerspective(0, SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_CENTER_X, SCREEN_CENTER_Y); + g_pSharedBGA->Draw(); + DISPLAY->CameraPopMatrix(); - for (unsigned i = 0; iDraw(); + for (unsigned i = 0; iDraw(); - for (unsigned i = 0; iDraw(); - } + for (unsigned i = 0; iDraw(); DISPLAY->EndFrame(); } diff --git a/src/Sprite.cpp b/src/Sprite.cpp index 37466a194e..70daa2b9f9 100644 --- a/src/Sprite.cpp +++ b/src/Sprite.cpp @@ -479,9 +479,6 @@ void TexCoordArrayFromRect( float fImageCoords[8], const RectF &rect ) void Sprite::DrawTexture( const TweenState *state ) { - if (!DISPLAY->ShouldRenderFrame()) - return; - Actor::SetGlobalRenderStates(); // set Actor-specified render states RectF crop = state->crop; @@ -625,9 +622,6 @@ bool Sprite::EarlyAbortDraw() const void Sprite::DrawPrimitives() { - if (!DISPLAY->ShouldRenderFrame()) - return; - if( m_pTempState->fade.top > 0 || m_pTempState->fade.bottom > 0 || m_pTempState->fade.left > 0 ||