From 5fd24b3f06ec48cb0a3d3cfbb4783d624270a691 Mon Sep 17 00:00:00 2001 From: Aiden Date: Wed, 6 May 2026 10:48:50 +1000 Subject: [PATCH] Hide renderer internals --- .../gl/OpenGLComposite.cpp | 38 ++++++------- .../gl/OpenGLRenderPass.cpp | 53 ++++++++++--------- .../gl/OpenGLRenderer.cpp | 7 +++ .../gl/OpenGLRenderer.h | 40 +++++++++++--- .../gl/OpenGLShaderPrograms.cpp | 20 ++++--- 5 files changed, 94 insertions(+), 64 deletions(-) diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp index c5d528b..cdb38a8 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp @@ -131,7 +131,7 @@ bool OpenGLComposite::InitDeckLink() } if (mDeckLink->inputFrameWidth != mDeckLink->outputFrameWidth || mDeckLink->inputFrameHeight != mDeckLink->outputFrameHeight) { - mRenderer->mFastTransferExtensionAvailable = false; + mRenderer->SetFastTransferAvailable(false); OutputDebugStringA("Input/output dimensions differ; using regular OpenGL transfer fallback instead of fast transfer.\n"); } @@ -151,10 +151,10 @@ bool OpenGLComposite::InitDeckLink() else resizeWindow(mDeckLink->outputFrameWidth / 2, mDeckLink->outputFrameHeight / 2); - if (mRenderer->mFastTransferExtensionAvailable) + if (mRenderer->FastTransferAvailable()) { // Initialize fast video frame transfers - if (! VideoFrameTransfer::initialize(mDeckLink->inputFrameWidth, mDeckLink->inputFrameHeight, mRenderer->mCaptureTexture, mRenderer->mOutputTexture)) + if (! VideoFrameTransfer::initialize(mDeckLink->inputFrameWidth, mDeckLink->inputFrameHeight, mRenderer->CaptureTexture(), mRenderer->OutputTexture())) { MessageBox(NULL, _T("Cannot initialize video transfers."), _T("VideoFrameTransfer error."), MB_OK); goto error; @@ -307,10 +307,10 @@ void OpenGLComposite::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bo wglMakeCurrent( hGLDC, hGLRC ); // make OpenGL context current in this thread - if (mRenderer->mFastTransferExtensionAvailable) + if (mRenderer->FastTransferAvailable()) { CComQIPtr allocator(inputFrameBuffer); - if (!allocator || !allocator->transferFrame(videoPixels, mRenderer->mCaptureTexture)) + if (!allocator || !allocator->transferFrame(videoPixels, mRenderer->CaptureTexture())) OutputDebugStringA("Capture: transferFrame() failed\n"); allocator->waitForTransferComplete(videoPixels); @@ -318,9 +318,9 @@ void OpenGLComposite::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bo else { // Use a straightforward texture buffer - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mRenderer->mUnpinnedTextureBuffer); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mRenderer->UnpinnedTextureBuffer()); glBufferData(GL_PIXEL_UNPACK_BUFFER, textureSize, videoPixels, GL_DYNAMIC_DRAW); - glBindTexture(GL_TEXTURE_2D, mRenderer->mCaptureTexture); + glBindTexture(GL_TEXTURE_2D, mRenderer->CaptureTexture()); // NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mDeckLink->inputFrameWidth / 2, mDeckLink->inputFrameHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); @@ -353,16 +353,16 @@ void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, // Draw the effect output to the off-screen framebuffer. const auto renderStartTime = std::chrono::steady_clock::now(); - if (mRenderer->mFastTransferExtensionAvailable) + if (mRenderer->FastTransferAvailable()) VideoFrameTransfer::beginTextureInUse(VideoFrameTransfer::GPUtoCPU); - glBindFramebuffer(GL_FRAMEBUFFER, mRenderer->mIdFrameBuf); + glBindFramebuffer(GL_FRAMEBUFFER, mRenderer->CompositeFramebuffer()); renderEffect(); - glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer->mIdFrameBuf); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer->mOutputFrameBuf); + glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer->CompositeFramebuffer()); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer->OutputFramebuffer()); glBlitFramebuffer(0, 0, mDeckLink->inputFrameWidth, mDeckLink->inputFrameHeight, 0, 0, mDeckLink->outputFrameWidth, mDeckLink->outputFrameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR); - glBindFramebuffer(GL_FRAMEBUFFER, mRenderer->mOutputFrameBuf); + glBindFramebuffer(GL_FRAMEBUFFER, mRenderer->OutputFramebuffer()); glFlush(); - if (mRenderer->mFastTransferExtensionAvailable) + if (mRenderer->FastTransferAvailable()) VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::GPUtoCPU); const auto renderEndTime = std::chrono::steady_clock::now(); if (mRuntimeHost) @@ -393,13 +393,13 @@ void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, void* pFrame; outputVideoFrameBuffer->GetBytes(&pFrame); - if (mRenderer->mFastTransferExtensionAvailable) + if (mRenderer->FastTransferAvailable()) { - // Finished with mRenderer->mCaptureTexture + // Finished sampling the capture texture for this frame. if (!mDeckLink->hasNoInputSource) VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::CPUtoGPU); - if (! mDeckLink->playoutAllocator->transferFrame(pFrame, mRenderer->mOutputTexture)) + if (! mDeckLink->playoutAllocator->transferFrame(pFrame, mRenderer->OutputTexture())) OutputDebugStringA("Playback: transferFrame() failed\n"); paintGL(); @@ -409,7 +409,7 @@ void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, } else { - glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer->mOutputFrameBuf); + glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer->OutputFramebuffer()); glReadPixels(0, 0, mDeckLink->outputFrameWidth, mDeckLink->outputFrameHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, pFrame); paintGL(); } @@ -551,9 +551,9 @@ void OpenGLComposite::resetTemporalHistoryState() bool OpenGLComposite::CheckOpenGLExtensions() { - mRenderer->mFastTransferExtensionAvailable = VideoFrameTransfer::checkFastMemoryTransferAvailable(); + mRenderer->SetFastTransferAvailable(VideoFrameTransfer::checkFastMemoryTransferAvailable()); - if (!mRenderer->mFastTransferExtensionAvailable) + if (!mRenderer->FastTransferAvailable()) OutputDebugStringA("Fast memory transfer extension not available, using regular OpenGL transfer fallback instead\n"); return true; diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderPass.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderPass.cpp index 9af1e7f..515bfbd 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderPass.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderPass.cpp @@ -17,9 +17,9 @@ void OpenGLRenderPass::Render( const TextBindingUpdater& updateTextBinding, const GlobalParamsUpdater& updateGlobalParams) { - if (hasInputSource && mRenderer.mFastTransferExtensionAvailable) + if (hasInputSource && mRenderer.FastTransferAvailable()) { - // Signal that we're about to draw using mCaptureTexture onto mFBOTexture. + // Signal that the capture texture is about to be sampled into the composite framebuffer. VideoFrameTransfer::beginTextureInUse(VideoFrameTransfer::CPUtoGPU); } @@ -31,31 +31,32 @@ void OpenGLRenderPass::Render( } else { - glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.mDecodeFrameBuf); + glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.DecodeFramebuffer()); glViewport(0, 0, inputFrameWidth, inputFrameHeight); glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); } - if (layerStates.empty() || mRenderer.mLayerPrograms.empty()) + std::vector& layerPrograms = mRenderer.LayerPrograms(); + if (layerStates.empty() || layerPrograms.empty()) { - glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.mDecodeFrameBuf); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.mIdFrameBuf); + glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.DecodeFramebuffer()); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.CompositeFramebuffer()); glBlitFramebuffer(0, 0, inputFrameWidth, inputFrameHeight, 0, 0, inputFrameWidth, inputFrameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR); - glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.mIdFrameBuf); + glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.CompositeFramebuffer()); } else { - GLuint sourceTexture = mRenderer.mDecodedTexture; - GLuint sourceFrameBuffer = mRenderer.mDecodeFrameBuf; - for (std::size_t index = 0; index < layerStates.size() && index < mRenderer.mLayerPrograms.size(); ++index) + GLuint sourceTexture = mRenderer.DecodedTexture(); + GLuint sourceFrameBuffer = mRenderer.DecodeFramebuffer(); + for (std::size_t index = 0; index < layerStates.size() && index < layerPrograms.size(); ++index) { const std::size_t remaining = layerStates.size() - index; const bool writeToMain = (remaining % 2) == 1; RenderShaderProgram( sourceTexture, - writeToMain ? mRenderer.mIdFrameBuf : mRenderer.mLayerTempFrameBuf, - mRenderer.mLayerPrograms[index], + writeToMain ? mRenderer.CompositeFramebuffer() : mRenderer.LayerTempFramebuffer(), + layerPrograms[index], layerStates[index], inputFrameWidth, inputFrameHeight, @@ -63,30 +64,30 @@ void OpenGLRenderPass::Render( updateTextBinding, updateGlobalParams); if (layerStates[index].temporalHistorySource == TemporalHistorySource::PreLayerInput) - mRenderer.mTemporalHistory.PushPreLayerFramebuffer(layerStates[index].layerId, sourceFrameBuffer, inputFrameWidth, inputFrameHeight); - sourceTexture = writeToMain ? mRenderer.mFBOTexture : mRenderer.mLayerTempTexture; - sourceFrameBuffer = writeToMain ? mRenderer.mIdFrameBuf : mRenderer.mLayerTempFrameBuf; + mRenderer.TemporalHistory().PushPreLayerFramebuffer(layerStates[index].layerId, sourceFrameBuffer, inputFrameWidth, inputFrameHeight); + sourceTexture = writeToMain ? mRenderer.CompositeTexture() : mRenderer.LayerTempTexture(); + sourceFrameBuffer = writeToMain ? mRenderer.CompositeFramebuffer() : mRenderer.LayerTempFramebuffer(); } } - mRenderer.mTemporalHistory.PushSourceFramebuffer(mRenderer.mDecodeFrameBuf, inputFrameWidth, inputFrameHeight); + mRenderer.TemporalHistory().PushSourceFramebuffer(mRenderer.DecodeFramebuffer(), inputFrameWidth, inputFrameHeight); - if (hasInputSource && mRenderer.mFastTransferExtensionAvailable) + if (hasInputSource && mRenderer.FastTransferAvailable()) VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::CPUtoGPU); } void OpenGLRenderPass::RenderDecodePass(unsigned inputFrameWidth, unsigned inputFrameHeight) { - glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.mDecodeFrameBuf); + glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.DecodeFramebuffer()); glViewport(0, 0, inputFrameWidth, inputFrameHeight); glClear(GL_COLOR_BUFFER_BIT); glActiveTexture(GL_TEXTURE0 + kPackedVideoTextureUnit); - glBindTexture(GL_TEXTURE_2D, mRenderer.mCaptureTexture); - glBindVertexArray(mRenderer.mFullscreenVAO); - glUseProgram(mRenderer.mDecodeProgram); + glBindTexture(GL_TEXTURE_2D, mRenderer.CaptureTexture()); + glBindVertexArray(mRenderer.FullscreenVertexArray()); + glUseProgram(mRenderer.DecodeProgram()); - const GLint packedResolutionLocation = glGetUniformLocation(mRenderer.mDecodeProgram, "uPackedVideoResolution"); - const GLint decodedResolutionLocation = glGetUniformLocation(mRenderer.mDecodeProgram, "uDecodedVideoResolution"); + const GLint packedResolutionLocation = glGetUniformLocation(mRenderer.DecodeProgram(), "uPackedVideoResolution"); + const GLint decodedResolutionLocation = glGetUniformLocation(mRenderer.DecodeProgram(), "uDecodedVideoResolution"); if (packedResolutionLocation >= 0) glUniform2f(packedResolutionLocation, static_cast(inputFrameWidth / 2), static_cast(inputFrameHeight)); if (decodedResolutionLocation >= 0) @@ -123,11 +124,11 @@ void OpenGLRenderPass::RenderShaderProgram( glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glActiveTexture(GL_TEXTURE0 + kDecodedVideoTextureUnit); glBindTexture(GL_TEXTURE_2D, sourceTexture); - mRenderer.mTemporalHistory.BindSamplers(state, sourceTexture, historyCap); + mRenderer.TemporalHistory().BindSamplers(state, sourceTexture, historyCap); BindLayerTextureAssets(layerProgram); - glBindVertexArray(mRenderer.mFullscreenVAO); + glBindVertexArray(mRenderer.FullscreenVertexArray()); glUseProgram(layerProgram.program); - updateGlobalParams(state, mRenderer.mTemporalHistory.SourceAvailableCount(), mRenderer.mTemporalHistory.AvailableCountForLayer(state.layerId)); + updateGlobalParams(state, mRenderer.TemporalHistory().SourceAvailableCount(), mRenderer.TemporalHistory().AvailableCountForLayer(state.layerId)); glDrawArrays(GL_TRIANGLES, 0, 3); glUseProgram(0); glBindVertexArray(0); diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderer.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderer.cpp index ee6c7a2..10888c1 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderer.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderer.cpp @@ -102,6 +102,13 @@ bool OpenGLRenderer::InitializeResources(unsigned inputFrameWidth, unsigned inpu return true; } +void OpenGLRenderer::SetDecodeShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader) +{ + mDecodeProgram = program; + mDecodeVertexShader = vertexShader; + mDecodeFragmentShader = fragmentShader; +} + void OpenGLRenderer::ResizeView(int width, int height) { mViewWidth = width; diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderer.h b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderer.h index dab9a76..53d0cec 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderer.h +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderer.h @@ -44,6 +44,38 @@ public: std::vector textBindings; }; + bool FastTransferAvailable() const { return mFastTransferExtensionAvailable; } + void SetFastTransferAvailable(bool available) { mFastTransferExtensionAvailable = available; } + GLuint CaptureTexture() const { return mCaptureTexture; } + GLuint DecodedTexture() const { return mDecodedTexture; } + GLuint LayerTempTexture() const { return mLayerTempTexture; } + GLuint CompositeTexture() const { return mFBOTexture; } + GLuint OutputTexture() const { return mOutputTexture; } + GLuint UnpinnedTextureBuffer() const { return mUnpinnedTextureBuffer; } + GLuint DecodeFramebuffer() const { return mDecodeFrameBuf; } + GLuint LayerTempFramebuffer() const { return mLayerTempFrameBuf; } + GLuint CompositeFramebuffer() const { return mIdFrameBuf; } + GLuint OutputFramebuffer() const { return mOutputFrameBuf; } + GLuint FullscreenVertexArray() const { return mFullscreenVAO; } + GLuint GlobalParamsUBO() const { return mGlobalParamsUBO; } + GLuint DecodeProgram() const { return mDecodeProgram; } + GLsizeiptr GlobalParamsUBOSize() const { return mGlobalParamsUBOSize; } + void SetGlobalParamsUBOSize(GLsizeiptr size) { mGlobalParamsUBOSize = size; } + void ReplaceLayerPrograms(std::vector& newPrograms) { mLayerPrograms.swap(newPrograms); } + std::vector& LayerPrograms() { return mLayerPrograms; } + const std::vector& LayerPrograms() const { return mLayerPrograms; } + TemporalHistoryBuffers& TemporalHistory() { return mTemporalHistory; } + const TemporalHistoryBuffers& TemporalHistory() const { return mTemporalHistory; } + void SetDecodeShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader); + bool InitializeResources(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned outputFrameWidth, unsigned outputFrameHeight, std::string& error); + void ResizeView(int width, int height); + void PresentToWindow(HDC hdc, unsigned outputFrameWidth, unsigned outputFrameHeight); + void DestroyResources(); + void DestroySingleLayerProgram(LayerProgram& layerProgram); + void DestroyLayerPrograms(); + void DestroyDecodeShaderProgram(); + +private: bool mFastTransferExtensionAvailable = false; GLuint mCaptureTexture = 0; GLuint mDecodedTexture = 0; @@ -67,12 +99,4 @@ public: int mViewHeight = 0; std::vector mLayerPrograms; TemporalHistoryBuffers mTemporalHistory; - - bool InitializeResources(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned outputFrameWidth, unsigned outputFrameHeight, std::string& error); - void ResizeView(int width, int height); - void PresentToWindow(HDC hdc, unsigned outputFrameWidth, unsigned outputFrameHeight); - void DestroyResources(); - void DestroySingleLayerProgram(LayerProgram& layerProgram); - void DestroyLayerPrograms(); - void DestroyDecodeShaderProgram(); }; diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLShaderPrograms.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLShaderPrograms.cpp index 0b3ed87..bdedd04 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLShaderPrograms.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLShaderPrograms.cpp @@ -62,12 +62,12 @@ bool OpenGLShaderPrograms::CompileLayerPrograms(unsigned inputFrameWidth, unsign const std::vector layerStates = mRuntimeHost.GetLayerRenderStates(inputFrameWidth, inputFrameHeight); std::string temporalError; const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames(); - if (!mRenderer.mTemporalHistory.ValidateTextureUnitBudget(layerStates, historyCap, temporalError)) + if (!mRenderer.TemporalHistory().ValidateTextureUnitBudget(layerStates, historyCap, temporalError)) { CopyErrorMessage(temporalError, errorMessageSize, errorMessage); return false; } - if (!mRenderer.mTemporalHistory.EnsureResources(layerStates, historyCap, inputFrameWidth, inputFrameHeight, temporalError)) + if (!mRenderer.TemporalHistory().EnsureResources(layerStates, historyCap, inputFrameWidth, inputFrameHeight, temporalError)) { CopyErrorMessage(temporalError, errorMessageSize, errorMessage); return false; @@ -89,7 +89,7 @@ bool OpenGLShaderPrograms::CompileLayerPrograms(unsigned inputFrameWidth, unsign } DestroyLayerPrograms(); - mRenderer.mLayerPrograms.swap(newPrograms); + mRenderer.ReplaceLayerPrograms(newPrograms); mRuntimeHost.SetCompileStatus(true, "Shader layers compiled successfully."); mRuntimeHost.ClearReloadRequest(); @@ -273,9 +273,7 @@ bool OpenGLShaderPrograms::CompileDecodeShader(int errorMessageSize, char* error } DestroyDecodeShaderProgram(); - mRenderer.mDecodeProgram = newProgram.release(); - mRenderer.mDecodeVertexShader = newVertexShader.release(); - mRenderer.mDecodeFragmentShader = newFragmentShader.release(); + mRenderer.SetDecodeShaderProgram(newProgram.release(), newVertexShader.release(), newFragmentShader.release()); return true; } @@ -296,7 +294,7 @@ void OpenGLShaderPrograms::DestroyDecodeShaderProgram() void OpenGLShaderPrograms::ResetTemporalHistoryState() { - mRenderer.mTemporalHistory.ResetState(); + mRenderer.TemporalHistory().ResetState(); } bool OpenGLShaderPrograms::LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error) @@ -410,17 +408,17 @@ bool OpenGLShaderPrograms::UpdateGlobalParamsBuffer(const RuntimeRenderState& st buffer.resize(AlignStd140(buffer.size(), 16), 0); - glBindBuffer(GL_UNIFORM_BUFFER, mRenderer.mGlobalParamsUBO); - if (mRenderer.mGlobalParamsUBOSize != static_cast(buffer.size())) + glBindBuffer(GL_UNIFORM_BUFFER, mRenderer.GlobalParamsUBO()); + if (mRenderer.GlobalParamsUBOSize() != static_cast(buffer.size())) { glBufferData(GL_UNIFORM_BUFFER, static_cast(buffer.size()), buffer.data(), GL_DYNAMIC_DRAW); - mRenderer.mGlobalParamsUBOSize = static_cast(buffer.size()); + mRenderer.SetGlobalParamsUBOSize(static_cast(buffer.size())); } else { glBufferSubData(GL_UNIFORM_BUFFER, 0, static_cast(buffer.size()), buffer.data()); } - glBindBufferBase(GL_UNIFORM_BUFFER, kGlobalParamsBindingPoint, mRenderer.mGlobalParamsUBO); + glBindBufferBase(GL_UNIFORM_BUFFER, kGlobalParamsBindingPoint, mRenderer.GlobalParamsUBO()); glBindBuffer(GL_UNIFORM_BUFFER, 0); return true;