Commit 0966f3f8 by Michael Spang Committed by Commit Bot

Vulkan: Remove flush semaphore chain

This avoids using an unbounded number of semaphores in between calls to swapbuffers. Using two semaphores should be sufficient to synchronize swaps. In addition, fix tracking of VkPipelineStageFlags by creating a 2nd vector parallel to the semaphores vector. The last fix assumed there could only be 2 wait semaphores, but that bound only applied to signal semaphores. After this change, there can only be one signal semaphore, but there's still no bound to wait semaphores. Bug: angleproject:3637 Change-Id: I7fbba67fa4bbdf62b9e9d530a924acd5236705d3 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/1688435 Commit-Queue: Michael Spang <spang@chromium.org> Reviewed-by: 's avatarShahbaz Youssefi <syoussefi@chromium.org>
parent 0c83813f
...@@ -76,30 +76,34 @@ constexpr size_t kInFlightCommandsLimit = 100u; ...@@ -76,30 +76,34 @@ constexpr size_t kInFlightCommandsLimit = 100u;
// Initially dumping the command graphs is disabled. // Initially dumping the command graphs is disabled.
constexpr bool kEnableCommandGraphDiagnostics = false; constexpr bool kEnableCommandGraphDiagnostics = false;
constexpr VkPipelineStageFlags kSubmitInfoWaitDstStageMask[] = {
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
};
void InitializeSubmitInfo(VkSubmitInfo *submitInfo, void InitializeSubmitInfo(VkSubmitInfo *submitInfo,
const vk::PrimaryCommandBuffer &commandBuffer, const vk::PrimaryCommandBuffer &commandBuffer,
const std::vector<VkSemaphore> &waitSemaphores, const std::vector<VkSemaphore> &waitSemaphores,
const SignalSemaphoreVector &signalSemaphores) std::vector<VkPipelineStageFlags> *waitSemaphoreStageMasks,
const vk::Semaphore *signalSemaphore)
{ {
// Verify that the submitInfo has been zero'd out.
ASSERT(submitInfo->signalSemaphoreCount == 0);
submitInfo->sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo->sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo->commandBufferCount = commandBuffer.valid() ? 1 : 0; submitInfo->commandBufferCount = commandBuffer.valid() ? 1 : 0;
submitInfo->pCommandBuffers = commandBuffer.ptr(); submitInfo->pCommandBuffers = commandBuffer.ptr();
static_assert(std::extent<decltype(kSubmitInfoWaitDstStageMask)>::value == if (waitSemaphoreStageMasks->size() < waitSemaphores.size())
std::decay_t<decltype(signalSemaphores)>::max_size(), {
"Wrong size for waitStageMask"); waitSemaphoreStageMasks->resize(waitSemaphores.size(),
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
}
submitInfo->waitSemaphoreCount = waitSemaphores.size(); submitInfo->waitSemaphoreCount = waitSemaphores.size();
submitInfo->pWaitSemaphores = waitSemaphores.data(); submitInfo->pWaitSemaphores = waitSemaphores.data();
submitInfo->pWaitDstStageMask = kSubmitInfoWaitDstStageMask; submitInfo->pWaitDstStageMask = waitSemaphoreStageMasks->data();
submitInfo->signalSemaphoreCount = signalSemaphores.size(); if (signalSemaphore)
submitInfo->pSignalSemaphores = signalSemaphores.data(); {
submitInfo->signalSemaphoreCount = 1;
submitInfo->pSignalSemaphores = signalSemaphore->ptr();
}
} }
uint32_t GetCoverageSampleCount(const gl::State &glState, FramebufferVk *drawFramebuffer) uint32_t GetCoverageSampleCount(const gl::State &glState, FramebufferVk *drawFramebuffer)
...@@ -359,7 +363,7 @@ angle::Result ContextVk::waitSemaphore(const gl::Context *context, ...@@ -359,7 +363,7 @@ angle::Result ContextVk::waitSemaphore(const gl::Context *context,
const GLuint *textures, const GLuint *textures,
const GLenum *srcLayouts) const GLenum *srcLayouts)
{ {
mWaitSemaphores.push_back(vk::GetImpl(semaphore)->getHandle()); addWaitSemaphore(vk::GetImpl(semaphore)->getHandle());
if (numBufferBarriers != 0) if (numBufferBarriers != 0)
{ {
...@@ -396,7 +400,7 @@ angle::Result ContextVk::signalSemaphore(const gl::Context *context, ...@@ -396,7 +400,7 @@ angle::Result ContextVk::signalSemaphore(const gl::Context *context,
UNIMPLEMENTED(); UNIMPLEMENTED();
} }
return flushImpl(semaphore); return flushImpl(vk::GetImpl(semaphore)->ptr());
} }
angle::Result ContextVk::setupDraw(const gl::Context *context, angle::Result ContextVk::setupDraw(const gl::Context *context,
...@@ -933,7 +937,8 @@ angle::Result ContextVk::synchronizeCpuGpuTime() ...@@ -933,7 +937,8 @@ angle::Result ContextVk::synchronizeCpuGpuTime()
// Submit the command buffer // Submit the command buffer
VkSubmitInfo submitInfo = {}; VkSubmitInfo submitInfo = {};
InitializeSubmitInfo(&submitInfo, commandBatch.get(), {}, {}); InitializeSubmitInfo(&submitInfo, commandBatch.get(), {}, &mWaitSemaphoreStageMasks,
nullptr);
ANGLE_TRY(submitFrame(submitInfo, std::move(commandBuffer))); ANGLE_TRY(submitFrame(submitInfo, std::move(commandBuffer)));
...@@ -2126,9 +2131,9 @@ const gl::ActiveTextureArray<TextureVk *> &ContextVk::getActiveTextures() const ...@@ -2126,9 +2131,9 @@ const gl::ActiveTextureArray<TextureVk *> &ContextVk::getActiveTextures() const
return mActiveTextures; return mActiveTextures;
} }
angle::Result ContextVk::flushImpl(const gl::Semaphore *clientSignalSemaphore) angle::Result ContextVk::flushImpl(const vk::Semaphore *signalSemaphore)
{ {
if (mCommandGraph.empty() && !clientSignalSemaphore && mWaitSemaphores.empty()) if (mCommandGraph.empty() && !signalSemaphore && mWaitSemaphores.empty())
{ {
return angle::Result::Continue; return angle::Result::Continue;
} }
...@@ -2141,16 +2146,11 @@ angle::Result ContextVk::flushImpl(const gl::Semaphore *clientSignalSemaphore) ...@@ -2141,16 +2146,11 @@ angle::Result ContextVk::flushImpl(const gl::Semaphore *clientSignalSemaphore)
ANGLE_TRY(flushCommandGraph(&commandBatch.get())); ANGLE_TRY(flushCommandGraph(&commandBatch.get()));
} }
SignalSemaphoreVector signalSemaphores; waitForSwapchainImageIfNecessary();
ANGLE_TRY(generateSurfaceSemaphores(&signalSemaphores));
if (clientSignalSemaphore)
{
signalSemaphores.push_back(vk::GetImpl(clientSignalSemaphore)->getHandle());
}
VkSubmitInfo submitInfo = {}; VkSubmitInfo submitInfo = {};
InitializeSubmitInfo(&submitInfo, commandBatch.get(), mWaitSemaphores, signalSemaphores); InitializeSubmitInfo(&submitInfo, commandBatch.get(), mWaitSemaphores,
&mWaitSemaphoreStageMasks, signalSemaphore);
ANGLE_TRY(submitFrame(submitInfo, commandBatch.release())); ANGLE_TRY(submitFrame(submitInfo, commandBatch.release()));
...@@ -2187,6 +2187,11 @@ angle::Result ContextVk::finishImpl() ...@@ -2187,6 +2187,11 @@ angle::Result ContextVk::finishImpl()
return angle::Result::Continue; return angle::Result::Continue;
} }
void ContextVk::addWaitSemaphore(VkSemaphore semaphore)
{
mWaitSemaphores.push_back(semaphore);
}
const vk::CommandPool &ContextVk::getCommandPool() const const vk::CommandPool &ContextVk::getCommandPool() const
{ {
return mCommandPool; return mCommandPool;
...@@ -2475,21 +2480,17 @@ angle::Result ContextVk::updateDefaultAttribute(size_t attribIndex) ...@@ -2475,21 +2480,17 @@ angle::Result ContextVk::updateDefaultAttribute(size_t attribIndex)
return angle::Result::Continue; return angle::Result::Continue;
} }
angle::Result ContextVk::generateSurfaceSemaphores(SignalSemaphoreVector *signalSemaphores) void ContextVk::waitForSwapchainImageIfNecessary()
{ {
if (mCurrentWindowSurface) if (mCurrentWindowSurface)
{ {
const vk::Semaphore *waitSemaphore = nullptr; vk::Semaphore waitSemaphore = mCurrentWindowSurface->getAcquireImageSemaphore();
const vk::Semaphore *signalSemaphore = nullptr; if (waitSemaphore.valid())
ANGLE_TRY(mCurrentWindowSurface->generateSemaphoresForFlush(this, &waitSemaphore, {
&signalSemaphore)); addWaitSemaphore(waitSemaphore.getHandle());
mWaitSemaphores.push_back(waitSemaphore->getHandle()); releaseObject(getCurrentQueueSerial(), &waitSemaphore);
}
ASSERT(signalSemaphores->empty());
signalSemaphores->push_back(signalSemaphore->getHandle());
} }
return angle::Result::Continue;
} }
vk::DescriptorSetLayoutDesc ContextVk::getDriverUniformsDescriptorSetDesc() const vk::DescriptorSetLayoutDesc ContextVk::getDriverUniformsDescriptorSetDesc() const
......
...@@ -241,9 +241,11 @@ class ContextVk : public ContextImpl, public vk::Context, public vk::CommandBuff ...@@ -241,9 +241,11 @@ class ContextVk : public ContextImpl, public vk::Context, public vk::CommandBuff
mLastIndexBufferOffset = reinterpret_cast<const void *>(angle::DirtyPointer); mLastIndexBufferOffset = reinterpret_cast<const void *>(angle::DirtyPointer);
} }
angle::Result flushImpl(const gl::Semaphore *semaphore); angle::Result flushImpl(const vk::Semaphore *semaphore);
angle::Result finishImpl(); angle::Result finishImpl();
void addWaitSemaphore(VkSemaphore semaphore);
const vk::CommandPool &getCommandPool() const; const vk::CommandPool &getCommandPool() const;
Serial getCurrentQueueSerial() const { return mCurrentQueueSerial; } Serial getCurrentQueueSerial() const { return mCurrentQueueSerial; }
...@@ -414,7 +416,7 @@ class ContextVk : public ContextImpl, public vk::Context, public vk::CommandBuff ...@@ -414,7 +416,7 @@ class ContextVk : public ContextImpl, public vk::Context, public vk::CommandBuff
void handleDeviceLost(); void handleDeviceLost();
angle::Result generateSurfaceSemaphores(SignalSemaphoreVector *signalSemaphores); void waitForSwapchainImageIfNecessary();
vk::PipelineHelper *mCurrentPipeline; vk::PipelineHelper *mCurrentPipeline;
gl::PrimitiveMode mCurrentDrawMode; gl::PrimitiveMode mCurrentDrawMode;
...@@ -588,6 +590,7 @@ class ContextVk : public ContextImpl, public vk::Context, public vk::CommandBuff ...@@ -588,6 +590,7 @@ class ContextVk : public ContextImpl, public vk::Context, public vk::CommandBuff
// Semaphores that must be waited on in the next submission. // Semaphores that must be waited on in the next submission.
std::vector<VkSemaphore> mWaitSemaphores; std::vector<VkSemaphore> mWaitSemaphores;
std::vector<VkPipelineStageFlags> mWaitSemaphoreStageMasks;
// Hold information from the last gpu clock sync for future gpu-to-cpu timestamp conversions. // Hold information from the last gpu clock sync for future gpu-to-cpu timestamp conversions.
struct GpuClockSyncInfo struct GpuClockSyncInfo
......
...@@ -27,6 +27,8 @@ class SemaphoreVk : public SemaphoreImpl ...@@ -27,6 +27,8 @@ class SemaphoreVk : public SemaphoreImpl
VkSemaphore getHandle() const { return mSemaphore.getHandle(); } VkSemaphore getHandle() const { return mSemaphore.getHandle(); }
const vk::Semaphore *ptr() const { return &mSemaphore; }
private: private:
angle::Result importOpaqueFd(gl::Context *context, GLint fd); angle::Result importOpaqueFd(gl::Context *context, GLint fd);
......
...@@ -331,18 +331,6 @@ WindowSurfaceVk::SwapchainImage::SwapchainImage(SwapchainImage &&other) ...@@ -331,18 +331,6 @@ WindowSurfaceVk::SwapchainImage::SwapchainImage(SwapchainImage &&other)
{} {}
WindowSurfaceVk::SwapHistory::SwapHistory() = default; WindowSurfaceVk::SwapHistory::SwapHistory() = default;
WindowSurfaceVk::SwapHistory::SwapHistory(SwapHistory &&other)
{
*this = std::move(other);
}
WindowSurfaceVk::SwapHistory &WindowSurfaceVk::SwapHistory::operator=(SwapHistory &&other)
{
std::swap(sharedFence, other.sharedFence);
std::swap(semaphores, other.semaphores);
std::swap(swapchain, other.swapchain);
return *this;
}
WindowSurfaceVk::SwapHistory::~SwapHistory() = default; WindowSurfaceVk::SwapHistory::~SwapHistory() = default;
...@@ -355,12 +343,7 @@ void WindowSurfaceVk::SwapHistory::destroy(VkDevice device) ...@@ -355,12 +343,7 @@ void WindowSurfaceVk::SwapHistory::destroy(VkDevice device)
} }
sharedFence.reset(device); sharedFence.reset(device);
presentImageSemaphore.destroy(device);
for (vk::Semaphore &semaphore : semaphores)
{
semaphore.destroy(device);
}
semaphores.clear();
} }
angle::Result WindowSurfaceVk::SwapHistory::waitFence(ContextVk *contextVk) angle::Result WindowSurfaceVk::SwapHistory::waitFence(ContextVk *contextVk)
...@@ -431,11 +414,7 @@ void WindowSurfaceVk::destroy(const egl::Display *display) ...@@ -431,11 +414,7 @@ void WindowSurfaceVk::destroy(const egl::Display *display)
mSurface = VK_NULL_HANDLE; mSurface = VK_NULL_HANDLE;
} }
for (vk::Semaphore &flushSemaphore : mFlushSemaphoreChain) mAcquireImageSemaphore.destroy(device);
{
flushSemaphore.destroy(device);
}
mFlushSemaphoreChain.clear();
} }
egl::Error WindowSurfaceVk::initialize(const egl::Display *display) egl::Error WindowSurfaceVk::initialize(const egl::Display *display)
...@@ -917,16 +896,14 @@ angle::Result WindowSurfaceVk::present(ContextVk *contextVk, ...@@ -917,16 +896,14 @@ angle::Result WindowSurfaceVk::present(ContextVk *contextVk,
} }
image.image.changeLayout(VK_IMAGE_ASPECT_COLOR_BIT, vk::ImageLayout::Present, swapCommands); image.image.changeLayout(VK_IMAGE_ASPECT_COLOR_BIT, vk::ImageLayout::Present, swapCommands);
ANGLE_TRY(contextVk->flushImpl(nullptr)); ANGLE_VK_TRY(contextVk, swap.presentImageSemaphore.init(contextVk->getDevice()));
// The semaphore chain must at least have the semaphore returned by vkAquireImage in it. It will ANGLE_TRY(contextVk->flushImpl(&swap.presentImageSemaphore));
// likely have more based on how much work was flushed this frame.
ASSERT(!mFlushSemaphoreChain.empty());
VkPresentInfoKHR presentInfo = {}; VkPresentInfoKHR presentInfo = {};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1; presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = mFlushSemaphoreChain.back().ptr(); presentInfo.pWaitSemaphores = swap.presentImageSemaphore.ptr();
presentInfo.swapchainCount = 1; presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = &mSwapchain; presentInfo.pSwapchains = &mSwapchain;
presentInfo.pImageIndices = &mCurrentSwapchainImageIndex; presentInfo.pImageIndices = &mCurrentSwapchainImageIndex;
...@@ -966,7 +943,7 @@ angle::Result WindowSurfaceVk::present(ContextVk *contextVk, ...@@ -966,7 +943,7 @@ angle::Result WindowSurfaceVk::present(ContextVk *contextVk,
// Update the swap history for this presentation // Update the swap history for this presentation
swap.sharedFence = contextVk->getLastSubmittedFence(); swap.sharedFence = contextVk->getLastSubmittedFence();
swap.semaphores = std::move(mFlushSemaphoreChain); ASSERT(!mAcquireImageSemaphore.valid());
++mCurrentSwapHistoryIndex; ++mCurrentSwapHistoryIndex;
mCurrentSwapHistoryIndex = mCurrentSwapHistoryIndex =
...@@ -1032,25 +1009,23 @@ VkResult WindowSurfaceVk::nextSwapchainImage(vk::Context *context) ...@@ -1032,25 +1009,23 @@ VkResult WindowSurfaceVk::nextSwapchainImage(vk::Context *context)
{ {
VkDevice device = context->getDevice(); VkDevice device = context->getDevice();
vk::Scoped<vk::Semaphore> aquireImageSemaphore(device); vk::Scoped<vk::Semaphore> acquireImageSemaphore(device);
VkResult result = aquireImageSemaphore.get().init(device); VkResult result = acquireImageSemaphore.get().init(device);
if (ANGLE_UNLIKELY(result != VK_SUCCESS)) if (ANGLE_UNLIKELY(result != VK_SUCCESS))
{ {
return result; return result;
} }
result = vkAcquireNextImageKHR(device, mSwapchain, UINT64_MAX, result = vkAcquireNextImageKHR(device, mSwapchain, UINT64_MAX,
aquireImageSemaphore.get().getHandle(), VK_NULL_HANDLE, acquireImageSemaphore.get().getHandle(), VK_NULL_HANDLE,
&mCurrentSwapchainImageIndex); &mCurrentSwapchainImageIndex);
if (ANGLE_UNLIKELY(result != VK_SUCCESS)) if (ANGLE_UNLIKELY(result != VK_SUCCESS))
{ {
return result; return result;
} }
// After presenting, the flush semaphore chain is cleared. The semaphore returned by // The semaphore will be waited on in the next flush.
// vkAcquireNextImage will start a new chain. mAcquireImageSemaphore = acquireImageSemaphore.release();
ASSERT(mFlushSemaphoreChain.empty());
mFlushSemaphoreChain.push_back(aquireImageSemaphore.release());
SwapchainImage &image = mSwapchainImages[mCurrentSwapchainImageIndex]; SwapchainImage &image = mSwapchainImages[mCurrentSwapchainImageIndex];
...@@ -1217,23 +1192,9 @@ angle::Result WindowSurfaceVk::getCurrentFramebuffer(vk::Context *context, ...@@ -1217,23 +1192,9 @@ angle::Result WindowSurfaceVk::getCurrentFramebuffer(vk::Context *context,
return angle::Result::Continue; return angle::Result::Continue;
} }
angle::Result WindowSurfaceVk::generateSemaphoresForFlush(vk::Context *context, vk::Semaphore WindowSurfaceVk::getAcquireImageSemaphore()
const vk::Semaphore **outWaitSemaphore,
const vk::Semaphore **outSignalSempahore)
{ {
// The flush semaphore chain should always start with a semaphore in it, created by the return std::move(mAcquireImageSemaphore);
// vkAquireImage call. This semaphore must be waited on before any rendering to the swap chain
// image can occur.
ASSERT(!mFlushSemaphoreChain.empty());
vk::Semaphore nextSemaphore;
ANGLE_VK_TRY(context, nextSemaphore.init(context->getDevice()));
mFlushSemaphoreChain.push_back(std::move(nextSemaphore));
*outWaitSemaphore = &mFlushSemaphoreChain[mFlushSemaphoreChain.size() - 2];
*outSignalSempahore = &mFlushSemaphoreChain[mFlushSemaphoreChain.size() - 1];
return angle::Result::Continue;
} }
angle::Result WindowSurfaceVk::initializeContents(const gl::Context *context, angle::Result WindowSurfaceVk::initializeContents(const gl::Context *context,
......
...@@ -142,9 +142,7 @@ class WindowSurfaceVk : public SurfaceVk ...@@ -142,9 +142,7 @@ class WindowSurfaceVk : public SurfaceVk
const vk::RenderPass &compatibleRenderPass, const vk::RenderPass &compatibleRenderPass,
vk::Framebuffer **framebufferOut); vk::Framebuffer **framebufferOut);
angle::Result generateSemaphoresForFlush(vk::Context *context, vk::Semaphore getAcquireImageSemaphore();
const vk::Semaphore **outWaitSemaphore,
const vk::Semaphore **outSignalSempahore);
protected: protected:
EGLNativeWindowType mNativeWindowType; EGLNativeWindowType mNativeWindowType;
...@@ -202,24 +200,7 @@ class WindowSurfaceVk : public SurfaceVk ...@@ -202,24 +200,7 @@ class WindowSurfaceVk : public SurfaceVk
}; };
std::vector<SwapchainImage> mSwapchainImages; std::vector<SwapchainImage> mSwapchainImages;
vk::Semaphore mAcquireImageSemaphore;
// Each time vkPresent is called, a wait semaphore is needed to know when the work to render the
// frame is done. For ANGLE to know when that is, it needs to add a signal semaphore to each
// flush. Conversely, before being able to use a swap chain image, ANGLE needs to wait on the
// semaphore returned by vkAcquireNextImage.
//
// We build a chain of semaphores starting with the semaphore returned by vkAcquireNextImageKHR
// and ending with the semaphore provided to vkPresent. Each time generateSemaphoresForFlush is
// called, a new semaphore is created and appended to mFlushSemaphoreChain. The second last
// semaphore is used as a wait semaphore and the last one is used as a signal semaphore for the
// flush.
//
// The semaphore chain is cleared after every call to present and a new one is started once
// vkAquireImage is called.
//
// We don't need a semaphore chain for offscreen surfaces or surfaceless rendering because the
// results cannot affect the images in a swap chain.
std::vector<vk::Semaphore> mFlushSemaphoreChain;
// A circular buffer that stores the serial of the renderer on every swap. The CPU is // A circular buffer that stores the serial of the renderer on every swap. The CPU is
// throttled by waiting for the 2nd previous serial to finish. Old swapchains are scheduled to // throttled by waiting for the 2nd previous serial to finish. Old swapchains are scheduled to
...@@ -227,8 +208,8 @@ class WindowSurfaceVk : public SurfaceVk ...@@ -227,8 +208,8 @@ class WindowSurfaceVk : public SurfaceVk
struct SwapHistory : angle::NonCopyable struct SwapHistory : angle::NonCopyable
{ {
SwapHistory(); SwapHistory();
SwapHistory(SwapHistory &&other); SwapHistory(SwapHistory &&other) = delete;
SwapHistory &operator=(SwapHistory &&other); SwapHistory &operator=(SwapHistory &&other) = delete;
~SwapHistory(); ~SwapHistory();
void destroy(VkDevice device); void destroy(VkDevice device);
...@@ -238,7 +219,8 @@ class WindowSurfaceVk : public SurfaceVk ...@@ -238,7 +219,8 @@ class WindowSurfaceVk : public SurfaceVk
// Fence associated with the last submitted work to render to this swapchain image. // Fence associated with the last submitted work to render to this swapchain image.
vk::Shared<vk::Fence> sharedFence; vk::Shared<vk::Fence> sharedFence;
std::vector<vk::Semaphore> semaphores; vk::Semaphore presentImageSemaphore;
VkSwapchainKHR swapchain = VK_NULL_HANDLE; VkSwapchainKHR swapchain = VK_NULL_HANDLE;
}; };
static constexpr size_t kSwapHistorySize = 2; static constexpr size_t kSwapHistorySize = 2;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment