blob: 0dee439866088e256c8961a599ac5a875d96ce4d [file] [log] [blame]
//
// Copyright 2016 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// SyncVk.cpp:
// Implements the class methods for SyncVk.
//
#include "libANGLE/renderer/vulkan/SyncVk.h"
#include "common/debug.h"
#include "libANGLE/Context.h"
#include "libANGLE/Display.h"
#include "libANGLE/renderer/vulkan/ContextVk.h"
#include "libANGLE/renderer/vulkan/DisplayVk.h"
#if !defined(ANGLE_PLATFORM_WINDOWS)
# include <poll.h>
# include <unistd.h>
#else
# include <io.h>
#endif
namespace
{
// Wait for file descriptor to be signaled
VkResult SyncWaitFd(int fd, uint64_t timeoutNs, VkResult timeoutResult = VK_TIMEOUT)
{
#if !defined(ANGLE_PLATFORM_WINDOWS)
struct pollfd fds;
int ret;
// Convert nanoseconds to milliseconds
int timeoutMs = static_cast<int>(timeoutNs / 1000000);
// If timeoutNs was non-zero but less than one millisecond, make it a millisecond.
if (timeoutNs > 0 && timeoutNs < 1000000)
{
timeoutMs = 1;
}
ASSERT(fd >= 0);
fds.fd = fd;
fds.events = POLLIN;
do
{
ret = poll(&fds, 1, timeoutMs);
if (ret > 0)
{
if (fds.revents & (POLLERR | POLLNVAL))
{
return VK_ERROR_UNKNOWN;
}
return VK_SUCCESS;
}
else if (ret == 0)
{
return timeoutResult;
}
} while (ret == -1 && (errno == EINTR || errno == EAGAIN));
return VK_ERROR_UNKNOWN;
#else
UNREACHABLE();
return VK_ERROR_UNKNOWN;
#endif
}
// Map VkResult to GLenum
void MapVkResultToGlenum(VkResult vkResult, angle::Result angleResult, void *outResult)
{
GLenum *glEnumOut = static_cast<GLenum *>(outResult);
ASSERT(glEnumOut);
if (angleResult != angle::Result::Continue)
{
*glEnumOut = GL_WAIT_FAILED;
return;
}
switch (vkResult)
{
case VK_EVENT_SET:
*glEnumOut = GL_ALREADY_SIGNALED;
break;
case VK_SUCCESS:
*glEnumOut = GL_CONDITION_SATISFIED;
break;
case VK_TIMEOUT:
*glEnumOut = GL_TIMEOUT_EXPIRED;
break;
default:
*glEnumOut = GL_WAIT_FAILED;
break;
}
}
// Map VkResult to EGLint
void MapVkResultToEglint(VkResult result, angle::Result angleResult, void *outResult)
{
EGLint *eglIntOut = static_cast<EGLint *>(outResult);
ASSERT(eglIntOut);
if (angleResult != angle::Result::Continue)
{
*eglIntOut = EGL_FALSE;
return;
}
switch (result)
{
case VK_EVENT_SET:
// fall through. EGL doesn't differentiate between event being already set, or set
// before timeout.
case VK_SUCCESS:
*eglIntOut = EGL_CONDITION_SATISFIED_KHR;
break;
case VK_TIMEOUT:
*eglIntOut = EGL_TIMEOUT_EXPIRED_KHR;
break;
default:
*eglIntOut = EGL_FALSE;
break;
}
}
} // anonymous namespace
namespace rx
{
namespace vk
{
SyncHelper::SyncHelper() {}
SyncHelper::~SyncHelper() {}
void SyncHelper::releaseToRenderer(Renderer *renderer) {}
angle::Result SyncHelper::initialize(ContextVk *contextVk, SyncFenceScope scope)
{
ASSERT(!mUse.valid());
return contextVk->onSyncObjectInit(this, scope);
}
angle::Result SyncHelper::prepareForClientWait(ErrorContext *context,
ContextVk *contextVk,
bool flushCommands,
uint64_t timeout,
VkResult *resultOut)
{
// If the event is already set, don't wait
bool alreadySignaled = false;
ANGLE_TRY(getStatus(context, contextVk, &alreadySignaled));
if (alreadySignaled)
{
*resultOut = VK_EVENT_SET;
return angle::Result::Continue;
}
// If timeout is zero, there's no need to wait, so return timeout already.
if (timeout == 0)
{
*resultOut = VK_TIMEOUT;
return angle::Result::Continue;
}
// Submit commands if requested
if (flushCommands && contextVk)
{
ANGLE_TRY(contextVk->flushCommandsAndEndRenderPassIfDeferredSyncInit(
RenderPassClosureReason::SyncObjectClientWait));
}
*resultOut = VK_INCOMPLETE;
return angle::Result::Continue;
}
angle::Result SyncHelper::clientWait(ErrorContext *context,
ContextVk *contextVk,
bool flushCommands,
uint64_t timeout,
MapVkResultToApiType mappingFunction,
void *resultOut)
{
ANGLE_TRACE_EVENT0("gpu.angle", "SyncHelper::clientWait");
VkResult status = VK_INCOMPLETE;
ANGLE_TRY(prepareForClientWait(context, contextVk, flushCommands, timeout, &status));
if (status != VK_INCOMPLETE)
{
mappingFunction(status, angle::Result::Continue, resultOut);
return angle::Result::Continue;
}
Renderer *renderer = context->getRenderer();
// If we need to perform a CPU wait don't set the resultOut parameter passed into the
// method, instead set the parameter passed into the unlocked tail call.
auto clientWaitUnlocked = [renderer, context, mappingFunction, use = mUse,
timeout](void *resultOut) {
ANGLE_TRACE_EVENT0("gpu.angle", "SyncHelper::clientWait block (unlocked)");
VkResult status = VK_INCOMPLETE;
angle::Result angleResult =
renderer->waitForResourceUseToFinishWithUserTimeout(context, use, timeout, &status);
// Note: resultOut may be nullptr through the glFinishFenceNV path, which does not have a
// return value.
if (resultOut != nullptr)
{
mappingFunction(status, angleResult, resultOut);
}
};
// Schedule the wait to be run at the tail of the current call.
egl::Display::GetCurrentThreadUnlockedTailCall()->add(clientWaitUnlocked);
return angle::Result::Continue;
}
angle::Result SyncHelper::finish(ContextVk *contextVk)
{
GLenum result;
return clientWait(contextVk, contextVk, true, UINT64_MAX, MapVkResultToGlenum, &result);
}
angle::Result SyncHelper::serverWait(ContextVk *contextVk)
{
// If already signaled, no need to wait
bool alreadySignaled = false;
ANGLE_TRY(getStatus(contextVk, contextVk, &alreadySignaled));
if (alreadySignaled)
{
return angle::Result::Continue;
}
// Every resource already tracks its usage and issues the appropriate barriers, so there's
// really nothing to do here. An execution barrier is issued to strictly satisfy what the
// application asked for.
vk::OutsideRenderPassCommandBuffer *commandBuffer;
ANGLE_TRY(contextVk->getOutsideRenderPassCommandBuffer({}, &commandBuffer));
commandBuffer->pipelineBarrier(VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 0,
nullptr);
return angle::Result::Continue;
}
angle::Result SyncHelper::getStatus(ErrorContext *context, ContextVk *contextVk, bool *signaledOut)
{
// Submit commands if it was deferred on the context that issued the sync object
ANGLE_TRY(submitSyncIfDeferred(contextVk, RenderPassClosureReason::SyncObjectClientWait));
ASSERT(mUse.valid());
Renderer *renderer = context->getRenderer();
if (renderer->hasResourceUseFinished(mUse))
{
*signaledOut = true;
}
else
{
// Check completed commands once before returning, perhaps the serial is actually already
// finished.
// We don't call checkCompletedCommandsAndCleanup() to cleanup finished commands immediately
// if isAsyncCommandBufferResetAndGarbageCleanupEnabled feature is turned off.
// Because when that feature is turned off, vkResetCommandBuffer() is called in cleanup
// step, and it must take the CommandPoolAccess::mCmdPoolMutex lock, see details in
// CommandPoolAccess::collectPrimaryCommandBuffer. This means the cleanup step can
// be blocked by command buffer recording if another thread calls
// CommandPoolAccess::flushRenderPassCommands(), which is against EGL spec where
// eglClientWaitSync() should return immediately with timeout == 0.
if (renderer->isAsyncCommandBufferResetAndGarbageCleanupEnabled())
{
ANGLE_TRY(renderer->checkCompletedCommandsAndCleanup(context));
}
else
{
ANGLE_TRY(renderer->checkCompletedCommands(context));
}
*signaledOut = renderer->hasResourceUseFinished(mUse);
}
return angle::Result::Continue;
}
angle::Result SyncHelper::submitSyncIfDeferred(ContextVk *contextVk, RenderPassClosureReason reason)
{
if (contextVk == nullptr)
{
return angle::Result::Continue;
}
if (contextVk->getRenderer()->hasResourceUseSubmitted(mUse))
{
return angle::Result::Continue;
}
// The submission of a sync object may be deferred to allow further optimizations to an open
// render pass before a submission happens for another reason. If the sync object is being
// waited on by the current context, the application must have used GL_SYNC_FLUSH_COMMANDS_BIT.
// However, when waited on by other contexts, the application must have ensured the original
// context is flushed. Due to deferred flushes, a glFlush is not sufficient to guarantee this.
//
// Deferring the submission is restricted to non-EGL sync objects, so it's sufficient to ensure
// that the contexts in the share group issue their deferred flushes.
for (auto context : contextVk->getShareGroup()->getContexts())
{
ContextVk *sharedContextVk = vk::GetImpl(context.second);
if (sharedContextVk->hasUnsubmittedUse(mUse))
{
ANGLE_TRY(sharedContextVk->flushCommandsAndEndRenderPassIfDeferredSyncInit(reason));
break;
}
}
// Note mUse could still be invalid here if it is inserted on a fresh created context, i.e.,
// fence is tracking nothing and is finished when inserted..
ASSERT(contextVk->getRenderer()->hasResourceUseSubmitted(mUse));
return angle::Result::Continue;
}
ExternalFence::ExternalFence()
: mDevice(VK_NULL_HANDLE), mFenceFdStatus(VK_INCOMPLETE), mFenceFd(kInvalidFenceFd)
{}
ExternalFence::~ExternalFence()
{
if (mDevice != VK_NULL_HANDLE)
{
mFence.destroy(mDevice);
}
if (mFenceFd != kInvalidFenceFd)
{
close(mFenceFd);
}
}
VkResult ExternalFence::init(VkDevice device, const VkFenceCreateInfo &createInfo)
{
ASSERT(device != VK_NULL_HANDLE);
ASSERT(mFenceFdStatus == VK_INCOMPLETE && mFenceFd == kInvalidFenceFd);
ASSERT(mDevice == VK_NULL_HANDLE);
mDevice = device;
return mFence.init(device, createInfo);
}
void ExternalFence::init(int fenceFd)
{
ASSERT(fenceFd != kInvalidFenceFd);
ASSERT(mFenceFdStatus == VK_INCOMPLETE && mFenceFd == kInvalidFenceFd);
mFenceFdStatus = VK_SUCCESS;
mFenceFd = fenceFd;
}
VkResult ExternalFence::getStatus(VkDevice device) const
{
if (mFenceFdStatus == VK_SUCCESS)
{
return SyncWaitFd(mFenceFd, 0, VK_NOT_READY);
}
return mFence.getStatus(device);
}
VkResult ExternalFence::wait(VkDevice device, uint64_t timeout) const
{
if (mFenceFdStatus == VK_SUCCESS)
{
return SyncWaitFd(mFenceFd, timeout);
}
return mFence.wait(device, timeout);
}
void ExternalFence::exportFd(VkDevice device, const VkFenceGetFdInfoKHR &fenceGetFdInfo)
{
ASSERT(mFenceFdStatus == VK_INCOMPLETE && mFenceFd == kInvalidFenceFd);
mFenceFdStatus = mFence.exportFd(device, fenceGetFdInfo, &mFenceFd);
ASSERT(mFenceFdStatus != VK_INCOMPLETE);
}
SyncHelperNativeFence::SyncHelperNativeFence()
{
mExternalFence = std::make_shared<ExternalFence>();
}
SyncHelperNativeFence::~SyncHelperNativeFence() {}
void SyncHelperNativeFence::releaseToRenderer(Renderer *renderer)
{
mExternalFence.reset();
}
angle::Result SyncHelperNativeFence::initializeWithFd(ContextVk *contextVk, int inFd)
{
ASSERT(inFd >= kInvalidFenceFd);
// If valid FD provided by application - import it to fence.
if (inFd > kInvalidFenceFd)
{
// File descriptor ownership: EGL_ANDROID_native_fence_sync
// Whenever a file descriptor is passed into or returned from an
// EGL call in this extension, ownership of that file descriptor is
// transferred. The recipient of the file descriptor must close it when it is
// no longer needed, and the provider of the file descriptor must dup it
// before providing it if they require continued use of the native fence.
mExternalFence->init(inFd);
return angle::Result::Continue;
}
Renderer *renderer = contextVk->getRenderer();
VkDevice device = renderer->getDevice();
VkExportFenceCreateInfo exportCreateInfo = {};
exportCreateInfo.sType = VK_STRUCTURE_TYPE_EXPORT_FENCE_CREATE_INFO;
exportCreateInfo.pNext = nullptr;
exportCreateInfo.handleTypes = VK_EXTERNAL_FENCE_HANDLE_TYPE_SYNC_FD_BIT_KHR;
// Create fenceInfo base.
VkFenceCreateInfo fenceCreateInfo = {};
fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceCreateInfo.flags = 0;
fenceCreateInfo.pNext = &exportCreateInfo;
// Initialize/create a VkFence handle
ANGLE_VK_TRY(contextVk, mExternalFence->init(device, fenceCreateInfo));
// invalid FD provided by application - create one with fence.
/*
Spec: "When a fence sync object is created or when an EGL native fence sync
object is created with the EGL_SYNC_NATIVE_FENCE_FD_ANDROID attribute set to
EGL_NO_NATIVE_FENCE_FD_ANDROID, eglCreateSyncKHR also inserts a fence command
into the command stream of the bound client API's current context and associates it
with the newly created sync object.
*/
// Flush current pending set of commands providing the fence...
ANGLE_TRY(contextVk->flushAndSubmitCommands(nullptr, &mExternalFence,
RenderPassClosureReason::SyncObjectWithFdInit));
ANGLE_VK_TRY(contextVk, mExternalFence->getFenceFdStatus());
return angle::Result::Continue;
}
angle::Result SyncHelperNativeFence::prepareForClientWait(ErrorContext *context,
ContextVk *contextVk,
bool flushCommands,
uint64_t timeout,
VkResult *resultOut)
{
// If already signaled, don't wait
bool alreadySignaled = false;
ANGLE_TRY(getStatus(context, contextVk, &alreadySignaled));
if (alreadySignaled)
{
*resultOut = VK_SUCCESS;
return angle::Result::Continue;
}
// If timeout is zero, there's no need to wait, so return timeout already.
if (timeout == 0)
{
*resultOut = VK_TIMEOUT;
return angle::Result::Continue;
}
if (flushCommands && contextVk)
{
ANGLE_TRY(contextVk->flushAndSubmitCommands(nullptr, nullptr,
RenderPassClosureReason::SyncObjectClientWait));
}
*resultOut = VK_INCOMPLETE;
return angle::Result::Continue;
}
angle::Result SyncHelperNativeFence::clientWait(ErrorContext *context,
ContextVk *contextVk,
bool flushCommands,
uint64_t timeout,
MapVkResultToApiType mappingFunction,
void *resultOut)
{
ANGLE_TRACE_EVENT0("gpu.angle", "SyncHelperNativeFence::clientWait");
VkResult status = VK_INCOMPLETE;
ANGLE_TRY(prepareForClientWait(context, contextVk, flushCommands, timeout, &status));
if (status != VK_INCOMPLETE)
{
mappingFunction(status, angle::Result::Continue, resultOut);
return angle::Result::Continue;
}
Renderer *renderer = context->getRenderer();
auto clientWaitUnlocked = [device = renderer->getDevice(), fence = mExternalFence,
mappingFunction, timeout](void *resultOut) {
ANGLE_TRACE_EVENT0("gpu.angle", "SyncHelperNativeFence::clientWait block (unlocked)");
ASSERT(resultOut);
VkResult status = fence->wait(device, timeout);
mappingFunction(status, angle::Result::Continue, resultOut);
};
egl::Display::GetCurrentThreadUnlockedTailCall()->add(clientWaitUnlocked);
return angle::Result::Continue;
}
angle::Result SyncHelperNativeFence::serverWait(ContextVk *contextVk)
{
Renderer *renderer = contextVk->getRenderer();
// If already signaled, no need to wait
bool alreadySignaled = false;
ANGLE_TRY(getStatus(contextVk, contextVk, &alreadySignaled));
if (alreadySignaled)
{
return angle::Result::Continue;
}
VkDevice device = renderer->getDevice();
DeviceScoped<Semaphore> waitSemaphore(device);
// Wait semaphore for next vkQueueSubmit().
// Create a Semaphore with imported fenceFd.
ANGLE_VK_TRY(contextVk, waitSemaphore.get().init(device));
VkImportSemaphoreFdInfoKHR importFdInfo = {};
importFdInfo.sType = VK_STRUCTURE_TYPE_IMPORT_SEMAPHORE_FD_INFO_KHR;
importFdInfo.semaphore = waitSemaphore.get().getHandle();
importFdInfo.flags = VK_SEMAPHORE_IMPORT_TEMPORARY_BIT_KHR;
importFdInfo.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT_KHR;
importFdInfo.fd = dup(mExternalFence->getFenceFd());
ANGLE_VK_TRY(contextVk, waitSemaphore.get().importFd(device, importFdInfo));
// Add semaphore to next submit job.
contextVk->addWaitSemaphore(waitSemaphore.get().getHandle(),
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT);
contextVk->addGarbage(&waitSemaphore.get()); // This releases the handle.
return angle::Result::Continue;
}
angle::Result SyncHelperNativeFence::getStatus(ErrorContext *context,
ContextVk *contextVk,
bool *signaledOut)
{
VkResult result = mExternalFence->getStatus(context->getDevice());
if (result != VK_NOT_READY)
{
ANGLE_VK_TRY(context, result);
}
*signaledOut = (result == VK_SUCCESS);
return angle::Result::Continue;
}
angle::Result SyncHelperNativeFence::dupNativeFenceFD(ErrorContext *context, int *fdOut) const
{
if (mExternalFence->getFenceFd() == kInvalidFenceFd)
{
return angle::Result::Stop;
}
*fdOut = dup(mExternalFence->getFenceFd());
return angle::Result::Continue;
}
} // namespace vk
SyncVk::SyncVk() : SyncImpl() {}
SyncVk::~SyncVk() {}
void SyncVk::onDestroy(const gl::Context *context)
{
mSyncHelper.releaseToRenderer(vk::GetImpl(context)->getRenderer());
}
angle::Result SyncVk::set(const gl::Context *context, GLenum condition, GLbitfield flags)
{
ASSERT(condition == GL_SYNC_GPU_COMMANDS_COMPLETE);
ASSERT(flags == 0);
return mSyncHelper.initialize(vk::GetImpl(context), SyncFenceScope::CurrentContextToShareGroup);
}
angle::Result SyncVk::clientWait(const gl::Context *context,
GLbitfield flags,
GLuint64 timeout,
GLenum *outResult)
{
ContextVk *contextVk = vk::GetImpl(context);
ASSERT((flags & ~GL_SYNC_FLUSH_COMMANDS_BIT) == 0);
bool flush = (flags & GL_SYNC_FLUSH_COMMANDS_BIT) != 0;
return mSyncHelper.clientWait(contextVk, contextVk, flush, static_cast<uint64_t>(timeout),
MapVkResultToGlenum, outResult);
}
angle::Result SyncVk::serverWait(const gl::Context *context, GLbitfield flags, GLuint64 timeout)
{
ASSERT(flags == 0);
ASSERT(timeout == GL_TIMEOUT_IGNORED);
ContextVk *contextVk = vk::GetImpl(context);
return mSyncHelper.serverWait(contextVk);
}
angle::Result SyncVk::getStatus(const gl::Context *context, GLint *outResult)
{
ContextVk *contextVk = vk::GetImpl(context);
bool signaled = false;
ANGLE_TRY(mSyncHelper.getStatus(contextVk, contextVk, &signaled));
*outResult = signaled ? GL_SIGNALED : GL_UNSIGNALED;
return angle::Result::Continue;
}
EGLSyncVk::EGLSyncVk() : EGLSyncImpl(), mSyncHelper(nullptr) {}
EGLSyncVk::~EGLSyncVk() {}
void EGLSyncVk::onDestroy(const egl::Display *display)
{
mSyncHelper->releaseToRenderer(vk::GetImpl(display)->getRenderer());
}
egl::Error EGLSyncVk::initialize(const egl::Display *display,
const gl::Context *context,
EGLenum type,
const egl::AttributeMap &attribs)
{
ASSERT(context != nullptr);
switch (type)
{
case EGL_SYNC_FENCE_KHR:
case EGL_SYNC_GLOBAL_FENCE_ANGLE:
{
vk::SyncHelper *syncHelper = new vk::SyncHelper();
mSyncHelper.reset(syncHelper);
const SyncFenceScope scope = type == EGL_SYNC_GLOBAL_FENCE_ANGLE
? SyncFenceScope::AllContextsToAllContexts
: SyncFenceScope::CurrentContextToAllContexts;
if (syncHelper->initialize(vk::GetImpl(context), scope) == angle::Result::Stop)
{
return egl::Error(EGL_BAD_ALLOC, "eglCreateSyncKHR failed to create sync object");
}
return egl::NoError();
}
case EGL_SYNC_NATIVE_FENCE_ANDROID:
{
vk::SyncHelperNativeFence *syncHelper = new vk::SyncHelperNativeFence();
mSyncHelper.reset(syncHelper);
EGLint nativeFenceFd =
attribs.getAsInt(EGL_SYNC_NATIVE_FENCE_FD_ANDROID, EGL_NO_NATIVE_FENCE_FD_ANDROID);
return angle::ToEGL(syncHelper->initializeWithFd(vk::GetImpl(context), nativeFenceFd),
EGL_BAD_ALLOC);
}
default:
UNREACHABLE();
return egl::Error(EGL_BAD_ALLOC);
}
}
egl::Error EGLSyncVk::clientWait(const egl::Display *display,
const gl::Context *context,
EGLint flags,
EGLTime timeout,
EGLint *outResult)
{
ASSERT((flags & ~EGL_SYNC_FLUSH_COMMANDS_BIT_KHR) == 0);
bool flush = (flags & EGL_SYNC_FLUSH_COMMANDS_BIT_KHR) != 0;
ContextVk *contextVk = context != nullptr && flush ? vk::GetImpl(context) : nullptr;
if (mSyncHelper->clientWait(vk::GetImpl(display), contextVk, flush,
static_cast<uint64_t>(timeout), MapVkResultToEglint,
outResult) == angle::Result::Stop)
{
return egl::Error(EGL_BAD_ALLOC);
}
return egl::NoError();
}
egl::Error EGLSyncVk::serverWait(const egl::Display *display,
const gl::Context *context,
EGLint flags)
{
// Server wait requires a valid bound context.
ASSERT(context);
// No flags are currently implemented.
ASSERT(flags == 0);
ContextVk *contextVk = vk::GetImpl(context);
return angle::ToEGL(mSyncHelper->serverWait(contextVk), EGL_BAD_ALLOC);
}
egl::Error EGLSyncVk::getStatus(const egl::Display *display, EGLint *outStatus)
{
bool signaled = false;
if (mSyncHelper->getStatus(vk::GetImpl(display), nullptr, &signaled) == angle::Result::Stop)
{
return egl::Error(EGL_BAD_ALLOC);
}
*outStatus = signaled ? EGL_SIGNALED_KHR : EGL_UNSIGNALED_KHR;
return egl::NoError();
}
egl::Error EGLSyncVk::dupNativeFenceFD(const egl::Display *display, EGLint *fdOut) const
{
return angle::ToEGL(mSyncHelper->dupNativeFenceFD(vk::GetImpl(display), fdOut),
EGL_BAD_PARAMETER);
}
} // namespace rx