Chromium Code Reviews| Index: ui/gl/async_pixel_transfer_delegate_android.cc |
| diff --git a/ui/gl/async_pixel_transfer_delegate_android.cc b/ui/gl/async_pixel_transfer_delegate_android.cc |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..594306fffe48371d7b81e02341edc50c6151db87 |
| --- /dev/null |
| +++ b/ui/gl/async_pixel_transfer_delegate_android.cc |
| @@ -0,0 +1,532 @@ |
| +// Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +#include "ui/gl/async_pixel_transfer_delegate_android.h" |
| + |
| +#include "base/bind.h" |
| +#include "base/cancelable_callback.h" |
| +#include "base/debug/trace_event.h" |
| +#include "base/lazy_instance.h" |
| +#include "base/logging.h" |
| +#include "base/memory/scoped_ptr.h" |
| +#include "base/process_util.h" |
| +#include "base/shared_memory.h" |
| +#include "base/threading/thread.h" |
| +#include "build/build_config.h" |
| +#include "third_party/angle/include/EGL/egl.h" |
| +#include "third_party/angle/include/EGL/eglext.h" |
| +#include "ui/gfx/point.h" |
| +#include "ui/gfx/size.h" |
| +#include "ui/gl/async_pixel_transfer_delegate.h" |
| +#include "ui/gl/egl_util.h" |
| +#include "ui/gl/gl_bindings.h" |
| +#include "ui/gl/gl_context.h" |
| +#include "ui/gl/gl_surface_egl.h" |
| +#include <sys/resource.h> |
| + |
| +using base::SharedMemory; |
| +using base::SharedMemoryHandle; |
| + |
| +namespace gfx { |
| + |
| +namespace { |
| + |
| +bool CheckErrors(const char* file, int line) { |
| + EGLint eglerror; |
| + GLenum glerror; |
| + bool success = true; |
| + while ((eglerror = eglGetError()) != EGL_SUCCESS) { |
| + LOG(ERROR) << "Async transfer eglerror at " |
| + << file << ":" << line << " " << eglerror; |
| + success = false; |
| + } |
| + while ((glerror = glGetError()) != GL_NO_ERROR) { |
| + LOG(ERROR) << "Async transfer openglerror at " |
| + << file << ":" << line << " " << glerror; |
| + success = false; |
| + } |
| + return success; |
| +} |
| +#define CHK() CheckErrors(__FILE__, __LINE__) |
| + |
| +// We duplicate shared memory to avoid use-after-free issues. This could also |
| +// be solved by ref-counting something, or with a destruction callback. There |
| +// wasn't an obvious hook or ref-counted container, so for now we dup/mmap. |
| +SharedMemory* DuplicateSharedMemory(SharedMemory* shared_memory, uint32 size) { |
| + // Duplicate the handle. |
| + SharedMemoryHandle duped_shared_memory_handle; |
| + if (!shared_memory->ShareToProcess( |
| + base::GetCurrentProcessHandle(), |
| + &duped_shared_memory_handle)) |
| + return NULL; |
| + scoped_ptr<SharedMemory> duped_shared_memory( |
| + new SharedMemory(duped_shared_memory_handle, false)); |
| + // Map the shared memory into this process. This validates the size. |
| + if (!duped_shared_memory->Map(size)) |
| + return NULL; |
| + return duped_shared_memory.release(); |
| +} |
| + |
| +// Gets the address of the data from shared memory. |
| +void* GetAddress(SharedMemory* shared_memory, |
| + uint32 shm_size, |
| + uint32 shm_data_offset, |
| + uint32 shm_data_size) { |
| + // Memory bounds have already been validated, so there |
| + // is just DCHECKS here. |
| + DCHECK(shared_memory); |
| + DCHECK(shared_memory->memory()); |
| + DCHECK(shm_data_offset + shm_data_size <= shm_size); |
|
greggman
2012/12/06 06:52:00
DCHECK_LE?
epenner
2012/12/08 03:15:04
Done.
|
| + return static_cast<int8*>(shared_memory->memory()) + shm_data_offset; |
| +} |
| + |
| +} // namespace |
| + |
| + |
| +// Class which holds async pixel transfers state (EGLImage). |
| +// The EGLImage is accessed by either thread, but the everything |
| +// else (and the lifetime of this object) is controlled only by the |
| +// main GPU thread. |
| +class AsyncTransferStateAndroid : public AsyncPixelTransferState { |
| + public: |
| + AsyncTransferStateAndroid(GLuint texture_id) |
| + : texture_id_(texture_id), |
| + needs_bind_(false), |
| + transfer_in_progress_(false), |
| + egl_image_(0) {} |
| + |
| + virtual ~AsyncTransferStateAndroid() { |
| + if (egl_image_) { |
|
greggman
2012/12/06 06:52:00
nit: As al pointed out. there's 3 spaces of indent
epenner
2012/12/08 03:15:04
Done.
|
| + EGLDisplay display = eglGetCurrentDisplay(); |
| + eglDestroyImageKHR(display, egl_image_); |
| + } |
| + } |
| + |
| + // implement AsyncPixelTransferState: |
| + virtual bool TransferIsInProgress() { |
| + return transfer_in_progress_; |
| + } |
| + |
| + virtual void BindTexture(GLenum target) { |
| + glBindTexture(target, texture_id_); |
| + if (needs_bind_) |
| + glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, egl_image_); |
| + needs_bind_ = false; |
| + } |
| + |
| + // Completion callbacks. |
| + void TexImage2DCompleted() { |
| + needs_bind_ = true; |
| + transfer_in_progress_ = false; |
| + } |
| + void TexSubImage2DCompleted() { |
| + transfer_in_progress_ = false; |
| + } |
| + |
| + // The 'real' texture. |
| + GLuint texture_id_; |
| + |
| + // Indicates there is a new EGLImage and the 'real' |
| + // texture needs to be bound to it as an EGLImage target. |
| + bool needs_bind_; |
| + |
| + // Indicates that an async transfer is in progress. |
| + bool transfer_in_progress_; |
| + |
| + // It would be nice if we could just create a new EGLImage for |
| + // every upload, but I found that didn't work, so this stores |
| + // one for the lifetime of the texture. |
| + EGLImageKHR egl_image_; |
| +}; |
| + |
| + |
| +// Class which handles async pixel transfers on Android (using |
| +// EGLImageKHR and another upload thread) |
| +class AsyncPixelTransferDelegateAndroid : public AsyncPixelTransferDelegate { |
| + public: |
| + AsyncPixelTransferDelegateAndroid(); |
| + virtual ~AsyncPixelTransferDelegateAndroid(); |
| + |
| + // implement AsyncPixelTransferDelegate: |
| + virtual scoped_refptr<AsyncPixelTransferState> |
| + CreatePixelTransferState(GLuint); |
| + |
| + virtual void AsyncNotifyCompletion( |
| + const base::Closure& task); |
| + |
| + virtual void AsyncTexImage2D( |
| + AsyncPixelTransferState*, |
| + GLenum target, |
| + GLint level, |
| + GLenum internal_format, |
| + GLsizei width, |
| + GLsizei height, |
| + GLint border, |
| + GLenum format, |
| + GLenum type, |
| + SharedMemory*, |
|
greggman
2012/12/06 06:52:00
should this be const?
epenner
2012/12/08 03:15:04
Done.
|
| + uint32 shm_size, |
| + uint32 shm_data_offset, |
| + uint32 shm_data_size); |
| + |
| + virtual void AsyncTexSubImage2D( |
| + AsyncPixelTransferState*, |
| + GLenum target, |
| + GLint level, |
| + GLint xoffset, |
| + GLint yoffset, |
| + GLsizei width, |
| + GLsizei height, |
| + GLenum format, |
| + GLenum type, |
| + SharedMemory*, |
|
greggman
2012/12/06 06:52:00
should this be const?
epenner
2012/12/08 03:15:04
Done.
|
| + uint32 shm_size, |
| + uint32 shm_data_offset, |
| + uint32 shm_data_size); |
| + |
| + private: |
| + void Initialize(); |
| + void Shutdown(); |
| + void PerformInitialize(); |
| + void PerformShutdown(); |
| + |
| + struct MemoryParams { |
|
greggman
2012/12/06 06:52:00
nit: type definition come before function declarat
epenner
2012/12/08 03:15:04
Done.
|
| + SharedMemory* shared_memory; uint32 shm_size; |
| + uint32 shm_data_offset; uint32 shm_data_size; |
| + }; |
| + struct AsyncTexImage2DParams { |
| + GLenum target; GLint level; GLenum internal_format; |
| + GLsizei width; GLsizei height; GLint border; GLenum format; GLenum type; |
| + }; |
| + struct AsyncTexSubImage2DParams { |
| + GLenum target; GLint level; GLint xoffset; GLint yoffset; |
| + GLsizei width; GLsizei height; GLenum format; GLenum type; |
| + }; |
| + void PerformAsyncTexImage2D( |
| + AsyncTransferStateAndroid*, |
| + AsyncTexImage2DParams, |
| + MemoryParams); |
| + void PerformAsyncTexSubImage2D( |
| + AsyncTransferStateAndroid*, |
| + AsyncTexSubImage2DParams, |
| + MemoryParams); |
| + |
| + scoped_ptr<base::Thread> thread_; |
| + scoped_refptr<gfx::GLContext> thread_context_; |
| + scoped_refptr<gfx::GLSurface> thread_surface_; |
| + |
| + DISALLOW_COPY_AND_ASSIGN(AsyncPixelTransferDelegateAndroid); |
| +}; |
| + |
| +// Lazy instance creation. |
| +base::LazyInstance<AsyncPixelTransferDelegateAndroid> |
| + g_async_pixel_transfer_delegate_ = LAZY_INSTANCE_INITIALIZER; |
| + |
| +AsyncPixelTransferDelegate* AsyncPixelTransferDelegate::Get() { |
| + return g_async_pixel_transfer_delegate_.Pointer(); |
| +} |
| + |
| +scoped_refptr<AsyncPixelTransferState> |
| + AsyncPixelTransferDelegateAndroid:: |
| + CreatePixelTransferState(GLuint texture_id) { |
| + return make_scoped_refptr(static_cast<AsyncPixelTransferState*>( |
| + new AsyncTransferStateAndroid(texture_id))); |
| +} |
| + |
| +AsyncPixelTransferDelegateAndroid::AsyncPixelTransferDelegateAndroid() |
| + : thread_(new base::Thread("GPUAsyncTransferThread")) |
| +{ |
| + Initialize(); |
| +} |
| + |
| +AsyncPixelTransferDelegateAndroid::~AsyncPixelTransferDelegateAndroid() |
| +{ |
| + Shutdown(); |
| +} |
| + |
| +void AsyncPixelTransferDelegateAndroid::Initialize() { |
| + // Start the thread and initialize on the thread. |
| + thread_->Start(); |
| + thread_->message_loop()->PostTask(FROM_HERE, base::Bind( |
| + &AsyncPixelTransferDelegateAndroid::PerformInitialize, |
| + base::Unretained(this))); |
| +} |
| + |
| +void AsyncPixelTransferDelegateAndroid::Shutdown() { |
| + // Shutdown and wait for the thread to finish. |
| + thread_->message_loop()->PostTask(FROM_HERE, base::Bind( |
| + &AsyncPixelTransferDelegateAndroid::PerformShutdown, |
| + base::Unretained(this))); |
| + thread_->Stop(); |
| +} |
| + |
| + |
| +namespace { |
| +// Dummy function to measure completion on |
| +// the upload thread. |
| +void NoOp() {} |
| +} // namespace |
| + |
| +void AsyncPixelTransferDelegateAndroid::AsyncNotifyCompletion( |
| + const base::Closure& task) { |
| + // Post a no-op task to the upload thread followed |
| + // by a reply to the callback. The reply will then occur after |
| + // all async transfers are complete. |
| + thread_->message_loop_proxy()->PostTaskAndReply(FROM_HERE, |
| + base::Bind(&NoOp), task); |
| +} |
| + |
| +void AsyncPixelTransferDelegateAndroid::AsyncTexImage2D( |
| + AsyncPixelTransferState* transfer_state, |
| + GLenum target, |
| + GLint level, |
| + GLenum internal_format, |
| + GLsizei width, |
| + GLsizei height, |
| + GLint border, |
| + GLenum format, |
| + GLenum type, |
| + SharedMemory* shared_memory, |
| + uint32 shm_size, |
| + uint32 shm_data_offset, |
| + uint32 shm_data_size) { |
| + AsyncTransferStateAndroid* state = |
| + static_cast<AsyncTransferStateAndroid*>(transfer_state); |
| + DCHECK(state); |
| + DCHECK(shared_memory); |
| + DCHECK(state->texture_id_); |
| + DCHECK(!state->transfer_in_progress_); |
| + DCHECK(target == GL_TEXTURE_2D); |
| + DCHECK(level == 0); |
| + |
| + state->transfer_in_progress_ = true; |
| + |
| + // Any existing EGLImage is made an 'orphan' by a call to texImage2D. We can |
| + // delete the existing one safely since the client texture is not affected. |
| + if (state->egl_image_) { |
| + EGLDisplay display = eglGetCurrentDisplay(); |
| + eglDestroyImageKHR(display, state->egl_image_); |
| + state->egl_image_ = 0; |
| + } |
| + |
| + AsyncTexImage2DParams tex_params = {target, level, internal_format, |
| + width, height, border, format, type}; |
| + MemoryParams mem_params = {DuplicateSharedMemory(shared_memory, shm_size), |
| + shm_size, shm_data_offset, shm_data_size}; |
| + |
| + thread_->message_loop_proxy()->PostTaskAndReply(FROM_HERE, |
| + base::Bind( |
| + &AsyncPixelTransferDelegateAndroid::PerformAsyncTexImage2D, |
| + base::Unretained(this), // The delegate always outlives its tasks. |
| + base::Unretained(state), // This is referenced in reply below. |
| + tex_params, |
| + mem_params), |
| + base::Bind( |
| + &AsyncTransferStateAndroid::TexImage2DCompleted, |
| + state)); |
| +} |
| + |
| +void AsyncPixelTransferDelegateAndroid::AsyncTexSubImage2D( |
| + AsyncPixelTransferState* transfer_state, |
| + GLenum target, |
| + GLint level, |
| + GLint xoffset, |
| + GLint yoffset, |
| + GLsizei width, |
| + GLsizei height, |
| + GLenum format, |
| + GLenum type, |
| + SharedMemory* shared_memory, |
| + uint32 shm_size, |
| + uint32 shm_data_offset, |
| + uint32 shm_data_size) { |
| + TRACE_EVENT2("gpu", "AsyncTexSubImage2D", |
| + "width", width, |
| + "height", height); |
| + AsyncTransferStateAndroid* state = |
| + static_cast<AsyncTransferStateAndroid*>(transfer_state); |
| + DCHECK(state); |
| + DCHECK(shared_memory); |
| + DCHECK(state->texture_id_); |
| + DCHECK(!state->transfer_in_progress_); |
| + DCHECK(target == GL_TEXTURE_2D); |
|
greggman
2012/12/06 06:52:00
DCHECK_EQ?
You probably need
DCHECK_EQ(static_ca
epenner
2012/12/08 03:15:04
Done.
|
| + DCHECK(level == 0); |
|
greggman
2012/12/06 06:52:00
DCHECK_EQ? or DCHECK(!level)?
The linter used to
epenner
2012/12/08 03:15:04
Done.
|
| + |
| + state->transfer_in_progress_ = true; |
| + |
| + // Create the EGLImage if it hasn't already been created. |
| + if (!state->egl_image_) { |
| + EGLDisplay egl_display = eglGetCurrentDisplay(); |
| + EGLContext egl_context = eglGetCurrentContext(); |
| + EGLenum egl_target = EGL_GL_TEXTURE_2D_KHR; |
| + EGLClientBuffer egl_buffer = |
| + reinterpret_cast<EGLClientBuffer>(state->texture_id_); |
| + EGLint egl_attrib_list[] = { |
| + EGL_GL_TEXTURE_LEVEL_KHR, level, // mip-map level to reference. |
| + EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, // preserve the data in the texture. |
| + EGL_NONE |
| + }; |
| + state->egl_image_ = eglCreateImageKHR( |
| + egl_display, |
| + egl_context, |
| + egl_target, |
| + egl_buffer, |
| + egl_attrib_list); |
| + } |
| + |
| + AsyncTexSubImage2DParams tex_params = {target, level, xoffset, yoffset, |
| + width, height, format, type}; |
|
greggman
2012/12/06 06:52:00
nit: indentation is off
epenner
2012/12/08 03:15:04
Done.
|
| + MemoryParams mem_params = {DuplicateSharedMemory(shared_memory, shm_size), |
| + shm_size, shm_data_offset, shm_data_size}; |
| + |
| + thread_->message_loop_proxy()->PostTaskAndReply(FROM_HERE, |
| + base::Bind( |
| + &AsyncPixelTransferDelegateAndroid::PerformAsyncTexSubImage2D, |
| + base::Unretained(this), // The delegate always outlives its tasks. |
| + base::Unretained(state), // This is referenced in reply below. |
| + tex_params, |
| + mem_params), |
| + base::Bind( |
| + &AsyncTransferStateAndroid::TexSubImage2DCompleted, |
| + state)); |
| +} |
| + |
| +void AsyncPixelTransferDelegateAndroid::PerformInitialize() { |
| + DCHECK(!thread_surface_); |
| + DCHECK(!thread_context_); |
| + |
| + // Lower the thread priority for uploads. |
| + // TODO: What's a good value? Without lowering this, uploads |
| + // would often execute immediately and block the GPU thread. |
| + setpriority(PRIO_PROCESS, base::PlatformThread::CurrentId(), 15); |
| + |
| + GLShareGroup* share_group = NULL; |
| + bool software = false; |
| + thread_surface_ = new gfx::PbufferGLSurfaceEGL(software, gfx::Size(1,1)); |
| + thread_surface_->Initialize(); |
| + thread_context_ = gfx::GLContext::CreateGLContext(share_group, |
| + thread_surface_, |
| + gfx::PreferDiscreteGpu); |
| + bool is_current = thread_context_->MakeCurrent(thread_surface_); |
| + |
| + DCHECK(thread_surface_); |
| + DCHECK(thread_context_); |
| + DCHECK(is_current); |
| +} |
| + |
| +void AsyncPixelTransferDelegateAndroid::PerformShutdown() { |
| + DCHECK(thread_surface_); |
| + DCHECK(thread_context_); |
| + thread_surface_ = NULL; |
| + thread_context_->ReleaseCurrent(thread_surface_); |
| + thread_context_ = NULL; |
| +} |
| + |
| +namespace { |
| +void WaitForGlFence() { |
| + // TODO: Fix bindings (link errors) to enable the code below. |
| + // TODO: Should we only sync just before we report completion? |
| + |
| + // Uploads usually finish on the CPU, but just in case add a fence |
| + // and guarantee the upload has completed. Flush bit is set to |
| + // insure we don't wait forever. |
| + // EGLSyncKHR fence = eglCreateSyncKHR(display, EGL_SYNC_FENCE_KHR, NULL); |
|
greggman
2012/12/06 11:22:20
btw: If you want these functions I think you need
epenner
2012/12/08 03:15:04
Done.
|
| + // EGLint flags = EGL_SYNC_FLUSH_COMMANDS_BIT_KHR; |
| + // EGLTimeKHR time = EGL_FOREVER_KHR; |
| + // eglClientWaitSyncKHR(display, fence, flags, time); |
| + glFinish(); |
| +} |
| +} // namespace |
| + |
| +void AsyncPixelTransferDelegateAndroid::PerformAsyncTexImage2D( |
| + AsyncTransferStateAndroid* state, |
| + AsyncTexImage2DParams t, |
|
greggman
2012/12/06 06:52:00
nit: except for i for index single letter names ar
epenner
2012/12/08 03:15:04
I'll change if you want but I was following the de
|
| + MemoryParams m) { |
| + // This is just to insure it is deleted. Could bind() do this? |
| + scoped_ptr<SharedMemory> shared_memory = make_scoped_ptr(m.shared_memory); |
| + void* data = GetAddress(m.shared_memory, m.shm_size, |
| + m.shm_data_offset, m.shm_data_size); |
| + |
| + // In texImage2D, we do everything on the upload thread. |
| + // This is because texImage2D can incur the allocation cost, and |
| + // it also 'orphans' any previous EGLImage bound to the texture. |
| + DCHECK(0 == t.level); |
|
greggman
2012/12/06 06:52:00
DCHECK_EQ?
epenner
2012/12/08 03:15:04
Done.
|
| + DCHECK(EGL_NO_IMAGE_KHR == state->egl_image_); |
|
greggman
2012/12/06 06:52:00
DCHECK_EQ?
epenner
2012/12/08 03:15:04
Done.
|
| + TRACE_EVENT2("gpu", "performAsyncTexImage2D", |
| + "width", t.width, |
| + "height", t.height); |
| + |
| + // Create a texture from the image and upload to it. |
| + GLuint temp_texture = 0; |
| + glGenTextures(1, &temp_texture); |
| + glActiveTexture(GL_TEXTURE0); |
| + glBindTexture(GL_TEXTURE_2D, temp_texture); |
| + { |
| + TRACE_EVENT0("gpu", "performAsyncTexSubImage2D glTexImage2D"); |
| + glTexImage2D(GL_TEXTURE_2D, |
| + t.level, t.internal_format, |
| + t.width, t.height, |
| + t.border, t.format, t.type, data); |
| + } |
| + |
| + // Create the EGLImage, as texSubImage always 'orphan's a previous EGLImage. |
| + EGLDisplay egl_display = eglGetCurrentDisplay(); |
| + EGLContext egl_context = eglGetCurrentContext(); |
| + EGLenum egl_target = EGL_GL_TEXTURE_2D_KHR; |
| + EGLClientBuffer egl_buffer = (EGLClientBuffer) temp_texture; |
| + EGLint egl_attrib_list[] = { |
| + EGL_GL_TEXTURE_LEVEL_KHR, t.level, // mip-map level to reference. |
| + EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, // preserve the data in the texture. |
| + EGL_NONE |
| + }; |
| + state->egl_image_ = eglCreateImageKHR( |
| + egl_display, |
| + egl_context, |
| + egl_target, |
| + egl_buffer, |
| + egl_attrib_list); |
| + WaitForGlFence(); |
| + |
| + // We can delete this thread's texture as the real texture |
| + // now contains the data. |
| + glDeleteTextures(1, &temp_texture); |
| +} |
| + |
| +void AsyncPixelTransferDelegateAndroid::PerformAsyncTexSubImage2D( |
| + AsyncTransferStateAndroid* state, |
| + AsyncTexSubImage2DParams t, |
| + MemoryParams m) { |
| + // This is just to insure it is deleted. Could bind() do this? |
| + scoped_ptr<SharedMemory> shared_memory = make_scoped_ptr(m.shared_memory); |
| + void* data = GetAddress(m.shared_memory, m.shm_size, |
| + m.shm_data_offset, m.shm_data_size); |
| + |
| + // For a texSubImage, the texture must already have been |
| + // created on the main thread, along with EGLImageKHR. |
| + DCHECK(EGL_NO_IMAGE_KHR != state->egl_image_); |
|
greggman
2012/12/06 06:52:00
DCHECK_EQ?
epenner
2012/12/08 03:15:04
Done.
|
| + DCHECK(0 == t.level); |
|
greggman
2012/12/06 06:52:00
same
epenner
2012/12/08 03:15:04
Done.
|
| + TRACE_EVENT2("gpu", "performAsyncTexSubImage2D", |
| + "width", t.width, |
| + "height", t.height); |
| + |
| + // Create a texture from the image and upload to it. |
| + GLuint temp_texture = 0; |
| + glGenTextures(1, &temp_texture); |
| + glActiveTexture(GL_TEXTURE0); |
| + glBindTexture(GL_TEXTURE_2D, temp_texture); |
| + glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, state->egl_image_); |
| + { |
| + TRACE_EVENT0("gpu", "performAsyncTexSubImage2D glTexSubImage2D"); |
| + glTexSubImage2D(GL_TEXTURE_2D, |
| + t.level, t.xoffset, t.yoffset, |
| + t.width, t.height, t.format, t.type, data); |
| + } |
| + WaitForGlFence(); |
| + |
| + // We can delete this thread's texture as the real texture |
| + // now contains the data. |
| + glDeleteTextures(1, &temp_texture); |
| +} |
| + |
| +} // namespace gfx |