Vulkan: Handle dirty RTs with state messages.
Prior to this CL we were handling dirty state change notifications by
flushing the RT Images just prior to use or just after they were
changed. This could lead to a few redundant checks in several places.
It also meant we needed an owner pointer from the RT to the parent
Image. This pointer would be null for Surfaces and Renderbuffers.
This cleans up the image flushing logic to be handled by dirty bit
notifications. When an app updates an attached Texture with TexSubImage
or related calls it will send a notification to the Framebuffer. The
Framebuffer then sets a dirty contents bit that is handled in the
implementation. In Vulkan this means flushing the dirty bits.
Requires adding a flag to the FramebufferImpl class to determine if we
need to syncState before we checkStatus. Adding the option allows us to
only call syncState for the GL back-end. Not calling syncState allows
the robust resource init operation to happen *before* we syncState.
Which in turn allows FramebuffeVk to initialize the VkImages in one go.
Added new regression tests for Texture updates. This might not cover
all cases. I found it was very hard to trigger some of the resource
update staging in TextureVk.
Bug: angleproject:3427
Change-Id: Idfa177436ba7fcb9d398f2b67922e085f778f82a
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/1601552
Commit-Queue: Jamie Madill <jmadill@chromium.org>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
diff --git a/src/libANGLE/Context.cpp b/src/libANGLE/Context.cpp
index a939ee1..1f45bc7 100644
--- a/src/libANGLE/Context.cpp
+++ b/src/libANGLE/Context.cpp
@@ -467,7 +467,9 @@
mClearDirtyBits.set(State::DIRTY_BIT_STENCIL_WRITEMASK_FRONT);
mClearDirtyBits.set(State::DIRTY_BIT_STENCIL_WRITEMASK_BACK);
mClearDirtyBits.set(State::DIRTY_BIT_DRAW_FRAMEBUFFER_BINDING);
- mClearDirtyObjects.set(State::DIRTY_OBJECT_DRAW_FRAMEBUFFER);
+
+ // We sync the draw Framebuffer manually in prepareForClear to allow the clear calls to do
+ // more custom handling for robust resource init.
mBlitDirtyBits.set(State::DIRTY_BIT_SCISSOR_TEST_ENABLED);
mBlitDirtyBits.set(State::DIRTY_BIT_SCISSOR);
@@ -3498,17 +3500,21 @@
angle::Result Context::prepareForClear(GLbitfield mask)
{
- ANGLE_TRY(syncDirtyObjects(mClearDirtyObjects));
+ // Sync the draw framebuffer manually after the clear attachments.
+ ASSERT(mClearDirtyObjects.none());
ANGLE_TRY(mState.getDrawFramebuffer()->ensureClearAttachmentsInitialized(this, mask));
+ ANGLE_TRY(mState.syncDirtyObject(this, GL_DRAW_FRAMEBUFFER));
ANGLE_TRY(syncDirtyBits(mClearDirtyBits));
return angle::Result::Continue;
}
angle::Result Context::prepareForClearBuffer(GLenum buffer, GLint drawbuffer)
{
- ANGLE_TRY(syncDirtyObjects(mClearDirtyObjects));
+ // Sync the draw framebuffer manually after the clear attachments.
+ ASSERT(mClearDirtyObjects.none());
ANGLE_TRY(mState.getDrawFramebuffer()->ensureClearBufferAttachmentsInitialized(this, buffer,
drawbuffer));
+ ANGLE_TRY(mState.syncDirtyObject(this, GL_DRAW_FRAMEBUFFER));
ANGLE_TRY(syncDirtyBits(mClearDirtyBits));
return angle::Result::Continue;
}
@@ -8190,8 +8196,11 @@
default:
if (index < kTextureMaxSubjectIndex)
{
- mState.onActiveTextureStateChange(this, index);
- mStateCache.onActiveTextureChange(this);
+ if (message != angle::SubjectMessage::ContentsChanged)
+ {
+ mState.onActiveTextureStateChange(this, index);
+ mStateCache.onActiveTextureChange(this);
+ }
}
else if (index < kUniformBufferMaxSubjectIndex)
{
diff --git a/src/libANGLE/Framebuffer.cpp b/src/libANGLE/Framebuffer.cpp
index 652bcfd..19f8a65 100644
--- a/src/libANGLE/Framebuffer.cpp
+++ b/src/libANGLE/Framebuffer.cpp
@@ -968,11 +968,16 @@
if (mCachedStatus.value() == GL_FRAMEBUFFER_COMPLETE)
{
- angle::Result err = syncState(context);
- if (err != angle::Result::Continue)
+ // We can skip syncState on several back-ends.
+ if (mImpl->shouldSyncStateBeforeCheckStatus())
{
- return 0;
+ angle::Result err = syncState(context);
+ if (err != angle::Result::Continue)
+ {
+ return 0;
+ }
}
+
if (!mImpl->checkStatus(context))
{
mCachedStatus = GL_FRAMEBUFFER_UNSUPPORTED;
@@ -1848,6 +1853,14 @@
{
if (message != angle::SubjectMessage::SubjectChanged)
{
+ // This can be triggered by SubImage calls for Textures.
+ if (message == angle::SubjectMessage::ContentsChanged)
+ {
+ mDirtyBits.set(DIRTY_BIT_COLOR_BUFFER_CONTENTS_0 + index);
+ onStateChange(context, angle::SubjectMessage::DirtyBitsFlagged);
+ return;
+ }
+
// This can be triggered by the GL back-end TextureGL class.
ASSERT(message == angle::SubjectMessage::DirtyBitsFlagged);
return;
diff --git a/src/libANGLE/Framebuffer.h b/src/libANGLE/Framebuffer.h
index 73c2b51..d88e11b 100644
--- a/src/libANGLE/Framebuffer.h
+++ b/src/libANGLE/Framebuffer.h
@@ -314,6 +314,11 @@
DIRTY_BIT_COLOR_ATTACHMENT_0 + IMPLEMENTATION_MAX_FRAMEBUFFER_ATTACHMENTS,
DIRTY_BIT_DEPTH_ATTACHMENT = DIRTY_BIT_COLOR_ATTACHMENT_MAX,
DIRTY_BIT_STENCIL_ATTACHMENT,
+ DIRTY_BIT_COLOR_BUFFER_CONTENTS_0,
+ DIRTY_BIT_COLOR_BUFFER_CONTENTS_MAX =
+ DIRTY_BIT_COLOR_BUFFER_CONTENTS_0 + IMPLEMENTATION_MAX_FRAMEBUFFER_ATTACHMENTS,
+ DIRTY_BIT_DEPTH_BUFFER_CONTENTS,
+ DIRTY_BIT_STENCIL_BUFFER_CONTENTS,
DIRTY_BIT_DRAW_BUFFERS,
DIRTY_BIT_READ_BUFFER,
DIRTY_BIT_DEFAULT_WIDTH,
diff --git a/src/libANGLE/Texture.cpp b/src/libANGLE/Texture.cpp
index 47c433e..e0bb7d2 100644
--- a/src/libANGLE/Texture.cpp
+++ b/src/libANGLE/Texture.cpp
@@ -1058,6 +1058,8 @@
ANGLE_TRY(handleMipmapGenerationHint(context, level));
+ onStateChange(context, angle::SubjectMessage::ContentsChanged);
+
return angle::Result::Continue;
}
@@ -1103,8 +1105,12 @@
ImageIndex index = ImageIndex::MakeFromTarget(target, level);
- return mTexture->setCompressedSubImage(context, index, area, format, unpackState, imageSize,
- pixels);
+ ANGLE_TRY(mTexture->setCompressedSubImage(context, index, area, format, unpackState, imageSize,
+ pixels));
+
+ onStateChange(context, angle::SubjectMessage::ContentsChanged);
+
+ return angle::Result::Continue;
}
angle::Result Texture::copyImage(Context *context,
@@ -1158,6 +1164,8 @@
ANGLE_TRY(mTexture->copySubImage(context, index, destOffset, sourceArea, source));
ANGLE_TRY(handleMipmapGenerationHint(context, index.getLevelIndex()));
+ onStateChange(context, angle::SubjectMessage::ContentsChanged);
+
return angle::Result::Continue;
}
@@ -1222,8 +1230,13 @@
ImageIndex index = ImageIndex::MakeFromTarget(target, level);
- return mTexture->copySubTexture(context, index, destOffset, sourceLevel, sourceBox, unpackFlipY,
- unpackPremultiplyAlpha, unpackUnmultiplyAlpha, source);
+ ANGLE_TRY(mTexture->copySubTexture(context, index, destOffset, sourceLevel, sourceBox,
+ unpackFlipY, unpackPremultiplyAlpha, unpackUnmultiplyAlpha,
+ source));
+
+ onStateChange(context, angle::SubjectMessage::ContentsChanged);
+
+ return angle::Result::Continue;
}
angle::Result Texture::copyCompressedTexture(Context *context, const Texture *source)
diff --git a/src/libANGLE/renderer/FramebufferImpl.h b/src/libANGLE/renderer/FramebufferImpl.h
index 63b48cd..b6db048 100644
--- a/src/libANGLE/renderer/FramebufferImpl.h
+++ b/src/libANGLE/renderer/FramebufferImpl.h
@@ -86,11 +86,23 @@
size_t index,
GLfloat *xy) const = 0;
+ // Special configuration option for checkStatus(). Some back-ends don't require a syncState
+ // before calling checkStatus. In practice the GL back-end is the only config that needs
+ // syncState because it depends on the behaviour of the driver. Allowing the Vulkan and
+ // D3D back-ends to skip syncState lets us do more work in the syncState call.
+ virtual bool shouldSyncStateBeforeCheckStatus() const;
+
const gl::FramebufferState &getState() const { return mState; }
protected:
const gl::FramebufferState &mState;
};
+
+inline bool FramebufferImpl::shouldSyncStateBeforeCheckStatus() const
+{
+ return false;
+}
+
} // namespace rx
#endif // LIBANGLE_RENDERER_FRAMEBUFFERIMPL_H_
diff --git a/src/libANGLE/renderer/RenderTargetCache.h b/src/libANGLE/renderer/RenderTargetCache.h
index f59aacf..e70ebf9 100644
--- a/src/libANGLE/renderer/RenderTargetCache.h
+++ b/src/libANGLE/renderer/RenderTargetCache.h
@@ -86,11 +86,13 @@
break;
default:
{
- ASSERT(gl::Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_0 == 0 &&
- dirtyBit < gl::Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_MAX);
- size_t colorIndex =
- static_cast<size_t>(dirtyBit - gl::Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_0);
- ANGLE_TRY(updateColorRenderTarget(context, state, colorIndex));
+ static_assert(gl::Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_0 == 0, "FB dirty bits");
+ if (dirtyBit < gl::Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_MAX)
+ {
+ size_t colorIndex = static_cast<size_t>(
+ dirtyBit - gl::Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_0);
+ ANGLE_TRY(updateColorRenderTarget(context, state, colorIndex));
+ }
break;
}
}
diff --git a/src/libANGLE/renderer/gl/FramebufferGL.cpp b/src/libANGLE/renderer/gl/FramebufferGL.cpp
index 4258e07..45c50ef 100644
--- a/src/libANGLE/renderer/gl/FramebufferGL.cpp
+++ b/src/libANGLE/renderer/gl/FramebufferGL.cpp
@@ -602,6 +602,11 @@
return angle::Result::Continue;
}
+bool FramebufferGL::shouldSyncStateBeforeCheckStatus() const
+{
+ return true;
+}
+
bool FramebufferGL::checkStatus(const gl::Context *context) const
{
const FunctionsGL *functions = GetFunctionsGL(context);
@@ -691,16 +696,19 @@
break;
default:
{
- ASSERT(Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_0 == 0 &&
- dirtyBit < Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_MAX);
- size_t index =
- static_cast<size_t>(dirtyBit - Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_0);
- const FramebufferAttachment *newAttachment = mState.getColorAttachment(index);
- BindFramebufferAttachment(
- functions, static_cast<GLenum>(GL_COLOR_ATTACHMENT0 + index), newAttachment);
- if (newAttachment)
+ static_assert(Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_0 == 0, "FB dirty bits");
+ if (dirtyBit < Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_MAX)
{
- attachment = newAttachment;
+ size_t index =
+ static_cast<size_t>(dirtyBit - Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_0);
+ const FramebufferAttachment *newAttachment = mState.getColorAttachment(index);
+ BindFramebufferAttachment(functions,
+ static_cast<GLenum>(GL_COLOR_ATTACHMENT0 + index),
+ newAttachment);
+ if (newAttachment)
+ {
+ attachment = newAttachment;
+ }
}
break;
}
diff --git a/src/libANGLE/renderer/gl/FramebufferGL.h b/src/libANGLE/renderer/gl/FramebufferGL.h
index 1ae9885..1c8e8b2 100644
--- a/src/libANGLE/renderer/gl/FramebufferGL.h
+++ b/src/libANGLE/renderer/gl/FramebufferGL.h
@@ -78,6 +78,9 @@
size_t index,
GLfloat *xy) const override;
+ // The GL back-end requires a full sync state before we call checkStatus.
+ bool shouldSyncStateBeforeCheckStatus() const override;
+
bool checkStatus(const gl::Context *context) const override;
angle::Result syncState(const gl::Context *context,
diff --git a/src/libANGLE/renderer/vulkan/FramebufferVk.cpp b/src/libANGLE/renderer/vulkan/FramebufferVk.cpp
index b563024..a6dfa6f 100644
--- a/src/libANGLE/renderer/vulkan/FramebufferVk.cpp
+++ b/src/libANGLE/renderer/vulkan/FramebufferVk.cpp
@@ -810,6 +810,35 @@
return true;
}
+angle::Result FramebufferVk::updateColorAttachment(const gl::Context *context, size_t colorIndexGL)
+{
+ ContextVk *contextVk = vk::GetImpl(context);
+
+ ANGLE_TRY(mRenderTargetCache.updateColorRenderTarget(context, mState, colorIndexGL));
+
+ // Update cached masks for masked clears.
+ RenderTargetVk *renderTarget = mRenderTargetCache.getColors()[colorIndexGL];
+ if (renderTarget)
+ {
+ const angle::Format &emulatedFormat = renderTarget->getImageFormat().imageFormat();
+ updateActiveColorMasks(colorIndexGL, emulatedFormat.redBits > 0,
+ emulatedFormat.greenBits > 0, emulatedFormat.blueBits > 0,
+ emulatedFormat.alphaBits > 0);
+
+ const angle::Format &sourceFormat = renderTarget->getImageFormat().angleFormat();
+ mEmulatedAlphaAttachmentMask.set(
+ colorIndexGL, sourceFormat.alphaBits == 0 && emulatedFormat.alphaBits > 0);
+
+ contextVk->updateColorMask(context->getState().getBlendState());
+ }
+ else
+ {
+ updateActiveColorMasks(colorIndexGL, false, false, false, false);
+ }
+
+ return angle::Result::Continue;
+}
+
angle::Result FramebufferVk::syncState(const gl::Context *context,
const gl::Framebuffer::DirtyBits &dirtyBits)
{
@@ -825,6 +854,10 @@
case gl::Framebuffer::DIRTY_BIT_STENCIL_ATTACHMENT:
ANGLE_TRY(mRenderTargetCache.updateDepthStencilRenderTarget(context, mState));
break;
+ case gl::Framebuffer::DIRTY_BIT_DEPTH_BUFFER_CONTENTS:
+ case gl::Framebuffer::DIRTY_BIT_STENCIL_BUFFER_CONTENTS:
+ ANGLE_TRY(mRenderTargetCache.getDepthStencil()->flushStagedUpdates(contextVk));
+ break;
case gl::Framebuffer::DIRTY_BIT_DRAW_BUFFERS:
case gl::Framebuffer::DIRTY_BIT_READ_BUFFER:
case gl::Framebuffer::DIRTY_BIT_DEFAULT_WIDTH:
@@ -834,33 +867,21 @@
break;
default:
{
- ASSERT(gl::Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_0 == 0 &&
- dirtyBit < gl::Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_MAX);
- size_t colorIndexGL =
- static_cast<size_t>(dirtyBit - gl::Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_0);
- ANGLE_TRY(
- mRenderTargetCache.updateColorRenderTarget(context, mState, colorIndexGL));
-
- // Update cached masks for masked clears.
- RenderTargetVk *renderTarget = mRenderTargetCache.getColors()[colorIndexGL];
- if (renderTarget)
+ static_assert(gl::Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_0 == 0, "FB dirty bits");
+ if (dirtyBit < gl::Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_MAX)
{
- const angle::Format &emulatedFormat =
- renderTarget->getImageFormat().imageFormat();
- updateActiveColorMasks(
- colorIndexGL, emulatedFormat.redBits > 0, emulatedFormat.greenBits > 0,
- emulatedFormat.blueBits > 0, emulatedFormat.alphaBits > 0);
-
- const angle::Format &sourceFormat =
- renderTarget->getImageFormat().angleFormat();
- mEmulatedAlphaAttachmentMask.set(
- colorIndexGL, sourceFormat.alphaBits == 0 && emulatedFormat.alphaBits > 0);
-
- contextVk->updateColorMask(context->getState().getBlendState());
+ size_t colorIndexGL = static_cast<size_t>(
+ dirtyBit - gl::Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_0);
+ ANGLE_TRY(updateColorAttachment(context, colorIndexGL));
}
else
{
- updateActiveColorMasks(colorIndexGL, false, false, false, false);
+ ASSERT(dirtyBit >= gl::Framebuffer::DIRTY_BIT_COLOR_BUFFER_CONTENTS_0 &&
+ dirtyBit < gl::Framebuffer::DIRTY_BIT_COLOR_BUFFER_CONTENTS_MAX);
+ size_t colorIndexGL = static_cast<size_t>(
+ dirtyBit - gl::Framebuffer::DIRTY_BIT_COLOR_BUFFER_CONTENTS_0);
+ ANGLE_TRY(mRenderTargetCache.getColors()[colorIndexGL]->flushStagedUpdates(
+ contextVk));
}
break;
}
@@ -1171,13 +1192,10 @@
RendererVk *renderer = contextVk->getRenderer();
- ANGLE_TRY(renderTarget->ensureImageInitialized(contextVk));
-
vk::CommandBuffer *commandBuffer = nullptr;
ANGLE_TRY(mFramebuffer.recordCommands(contextVk, &commandBuffer));
// Note that although we're reading from the image, we need to update the layout below.
-
vk::ImageHelper *srcImage =
renderTarget->getImageForRead(&mFramebuffer, vk::ImageLayout::TransferSrc, commandBuffer);
diff --git a/src/libANGLE/renderer/vulkan/FramebufferVk.h b/src/libANGLE/renderer/vulkan/FramebufferVk.h
index 1677f82..a101dec 100644
--- a/src/libANGLE/renderer/vulkan/FramebufferVk.h
+++ b/src/libANGLE/renderer/vulkan/FramebufferVk.h
@@ -186,6 +186,7 @@
uint8_t clearStencilValue);
void updateActiveColorMasks(size_t colorIndex, bool r, bool g, bool b, bool a);
void updateRenderPassDesc();
+ angle::Result updateColorAttachment(const gl::Context *context, size_t colorIndex);
WindowSurfaceVk *mBackbuffer;
diff --git a/src/libANGLE/renderer/vulkan/RenderTargetVk.cpp b/src/libANGLE/renderer/vulkan/RenderTargetVk.cpp
index bafcda6..9a00f2a 100644
--- a/src/libANGLE/renderer/vulkan/RenderTargetVk.cpp
+++ b/src/libANGLE/renderer/vulkan/RenderTargetVk.cpp
@@ -10,6 +10,7 @@
#include "libANGLE/renderer/vulkan/RenderTargetVk.h"
#include "libANGLE/renderer/vulkan/CommandGraph.h"
+#include "libANGLE/renderer/vulkan/ContextVk.h"
#include "libANGLE/renderer/vulkan/TextureVk.h"
#include "libANGLE/renderer/vulkan/vk_format_utils.h"
#include "libANGLE/renderer/vulkan/vk_helpers.h"
@@ -17,7 +18,7 @@
namespace rx
{
RenderTargetVk::RenderTargetVk()
- : mImage(nullptr), mImageView(nullptr), mLevelIndex(0), mLayerIndex(0), mOwner(nullptr)
+ : mImage(nullptr), mImageView(nullptr), mLevelIndex(0), mLayerIndex(0)
{}
RenderTargetVk::~RenderTargetVk() {}
@@ -26,21 +27,18 @@
: mImage(other.mImage),
mImageView(other.mImageView),
mLevelIndex(other.mLevelIndex),
- mLayerIndex(other.mLayerIndex),
- mOwner(other.mOwner)
+ mLayerIndex(other.mLayerIndex)
{}
void RenderTargetVk::init(vk::ImageHelper *image,
vk::ImageView *imageView,
size_t levelIndex,
- size_t layerIndex,
- TextureVk *owner)
+ size_t layerIndex)
{
mImage = image;
mImageView = imageView;
mLevelIndex = levelIndex;
mLayerIndex = layerIndex;
- mOwner = owner;
}
void RenderTargetVk::reset()
@@ -49,7 +47,6 @@
mImageView = nullptr;
mLevelIndex = 0;
mLayerIndex = 0;
- mOwner = nullptr;
}
angle::Result RenderTargetVk::onColorDraw(ContextVk *contextVk,
@@ -59,8 +56,6 @@
ASSERT(commandBuffer->valid());
ASSERT(!mImage->getFormat().imageFormat().hasDepthOrStencilBits());
- ANGLE_TRY(ensureImageInitialized(contextVk));
-
// TODO(jmadill): Use automatic layout transition. http://anglebug.com/2361
mImage->changeLayout(VK_IMAGE_ASPECT_COLOR_BIT, vk::ImageLayout::ColorAttachment,
commandBuffer);
@@ -82,8 +77,6 @@
const angle::Format &format = mImage->getFormat().imageFormat();
VkImageAspectFlags aspectFlags = vk::GetDepthStencilAspectFlags(format);
- ANGLE_TRY(ensureImageInitialized(contextVk));
-
mImage->changeLayout(aspectFlags, vk::ImageLayout::DepthStencilAttachment, commandBuffer);
// Set up dependencies between the RT resource and the Framebuffer.
@@ -132,7 +125,6 @@
ASSERT(image && image->valid() && imageView && imageView->valid());
mImage = image;
mImageView = imageView;
- mOwner = nullptr;
}
vk::ImageHelper *RenderTargetVk::getImageForRead(vk::CommandGraphResource *readingResource,
@@ -170,16 +162,16 @@
return mImage;
}
-angle::Result RenderTargetVk::ensureImageInitialized(ContextVk *contextVk)
+angle::Result RenderTargetVk::flushStagedUpdates(ContextVk *contextVk)
{
- if (mOwner)
- {
- // If the render target source is a texture, make sure the image is initialized and its
- // staged updates flushed.
- return mOwner->ensureImageInitialized(contextVk);
- }
+ ASSERT(mImage->valid());
+ if (!mImage->hasStagedUpdates())
+ return angle::Result::Continue;
- return angle::Result::Continue;
+ vk::CommandBuffer *commandBuffer;
+ ANGLE_TRY(mImage->recordCommands(contextVk, &commandBuffer));
+ return mImage->flushStagedUpdates(contextVk, mLevelIndex, mLevelIndex + 1, mLayerIndex,
+ mLayerIndex + 1, commandBuffer);
}
} // namespace rx
diff --git a/src/libANGLE/renderer/vulkan/RenderTargetVk.h b/src/libANGLE/renderer/vulkan/RenderTargetVk.h
index 28ece88..4bfda37 100644
--- a/src/libANGLE/renderer/vulkan/RenderTargetVk.h
+++ b/src/libANGLE/renderer/vulkan/RenderTargetVk.h
@@ -46,8 +46,7 @@
void init(vk::ImageHelper *image,
vk::ImageView *imageView,
size_t levelIndex,
- size_t layerIndex,
- TextureVk *owner);
+ size_t layerIndex);
void reset();
// Note: RenderTargets should be called in order, with the depth/stencil onRender last.
@@ -79,7 +78,7 @@
// RenderTargetVk pointer.
void updateSwapchainImage(vk::ImageHelper *image, vk::ImageView *imageView);
- angle::Result ensureImageInitialized(ContextVk *contextVk);
+ angle::Result flushStagedUpdates(ContextVk *contextVk);
private:
vk::ImageHelper *mImage;
@@ -88,10 +87,6 @@
vk::ImageView *mImageView;
size_t mLevelIndex;
size_t mLayerIndex;
-
- // If owned by the texture, this will be non-nullptr, and is used to ensure texture changes
- // are flushed.
- TextureVk *mOwner;
};
} // namespace rx
diff --git a/src/libANGLE/renderer/vulkan/RenderbufferVk.cpp b/src/libANGLE/renderer/vulkan/RenderbufferVk.cpp
index 3e911b8..53e1a5f 100644
--- a/src/libANGLE/renderer/vulkan/RenderbufferVk.cpp
+++ b/src/libANGLE/renderer/vulkan/RenderbufferVk.cpp
@@ -85,10 +85,9 @@
&mImageView, 0, 1));
// Clear the renderbuffer if it has emulated channels.
- ANGLE_TRY(mImage->clearIfEmulatedFormat(vk::GetImpl(context), gl::ImageIndex::Make2D(0),
- vkFormat));
+ mImage->clearIfEmulatedFormat(vk::GetImpl(context), gl::ImageIndex::Make2D(0), vkFormat);
- mRenderTarget.init(mImage, &mImageView, 0, 0, nullptr);
+ mRenderTarget.init(mImage, &mImageView, 0, 0);
}
return angle::Result::Continue;
@@ -135,8 +134,7 @@
gl::SwizzleState(), &mImageView, imageVk->getImageLevel(),
1, imageVk->getImageLayer(), 1));
- mRenderTarget.init(mImage, &mImageView, imageVk->getImageLevel(), imageVk->getImageLayer(),
- nullptr);
+ mRenderTarget.init(mImage, &mImageView, imageVk->getImageLevel(), imageVk->getImageLayer());
return angle::Result::Continue;
}
@@ -147,6 +145,7 @@
FramebufferAttachmentRenderTarget **rtOut)
{
ASSERT(mImage && mImage->valid());
+ ANGLE_TRY(mRenderTarget.flushStagedUpdates(vk::GetImpl(context)));
*rtOut = &mRenderTarget;
return angle::Result::Continue;
}
diff --git a/src/libANGLE/renderer/vulkan/SurfaceVk.cpp b/src/libANGLE/renderer/vulkan/SurfaceVk.cpp
index f13379c..01d90a0 100644
--- a/src/libANGLE/renderer/vulkan/SurfaceVk.cpp
+++ b/src/libANGLE/renderer/vulkan/SurfaceVk.cpp
@@ -87,11 +87,34 @@
} // namespace
-OffscreenSurfaceVk::AttachmentImage::AttachmentImage()
+SurfaceVk::SurfaceVk(const egl::SurfaceState &surfaceState) : SurfaceImpl(surfaceState) {}
+
+SurfaceVk::~SurfaceVk() = default;
+
+angle::Result SurfaceVk::getAttachmentRenderTarget(const gl::Context *context,
+ GLenum binding,
+ const gl::ImageIndex &imageIndex,
+ FramebufferAttachmentRenderTarget **rtOut)
{
- renderTarget.init(&image, &imageView, 0, 0, nullptr);
+ ContextVk *contextVk = vk::GetImpl(context);
+
+ if (binding == GL_BACK)
+ {
+ ANGLE_TRY(mColorRenderTarget.flushStagedUpdates(contextVk));
+ *rtOut = &mColorRenderTarget;
+ }
+ else
+ {
+ ASSERT(binding == GL_DEPTH || binding == GL_STENCIL || binding == GL_DEPTH_STENCIL);
+ ANGLE_TRY(mDepthStencilRenderTarget.flushStagedUpdates(contextVk));
+ *rtOut = &mDepthStencilRenderTarget;
+ }
+
+ return angle::Result::Continue;
}
+OffscreenSurfaceVk::AttachmentImage::AttachmentImage() {}
+
OffscreenSurfaceVk::AttachmentImage::~AttachmentImage() = default;
angle::Result OffscreenSurfaceVk::AttachmentImage::initialize(DisplayVk *displayVk,
@@ -120,7 +143,7 @@
&imageView, 0, 1));
// Clear the image if it has emulated channels.
- ANGLE_TRY(image.clearIfEmulatedFormat(displayVk, gl::ImageIndex::Make2D(0), vkFormat));
+ image.clearIfEmulatedFormat(displayVk, gl::ImageIndex::Make2D(0), vkFormat);
return angle::Result::Continue;
}
@@ -138,8 +161,12 @@
OffscreenSurfaceVk::OffscreenSurfaceVk(const egl::SurfaceState &surfaceState,
EGLint width,
EGLint height)
- : SurfaceImpl(surfaceState), mWidth(width), mHeight(height)
-{}
+ : SurfaceVk(surfaceState), mWidth(width), mHeight(height)
+{
+ mColorRenderTarget.init(&mColorAttachment.image, &mColorAttachment.imageView, 0, 0);
+ mDepthStencilRenderTarget.init(&mDepthStencilAttachment.image,
+ &mDepthStencilAttachment.imageView, 0, 0);
+}
OffscreenSurfaceVk::~OffscreenSurfaceVk() {}
@@ -250,25 +277,6 @@
return EGL_BUFFER_DESTROYED;
}
-angle::Result OffscreenSurfaceVk::getAttachmentRenderTarget(
- const gl::Context *context,
- GLenum binding,
- const gl::ImageIndex &imageIndex,
- FramebufferAttachmentRenderTarget **rtOut)
-{
- if (binding == GL_BACK)
- {
- *rtOut = &mColorAttachment.renderTarget;
- }
- else
- {
- ASSERT(binding == GL_DEPTH || binding == GL_STENCIL || binding == GL_DEPTH_STENCIL);
- *rtOut = &mDepthStencilAttachment.renderTarget;
- }
-
- return angle::Result::Continue;
-}
-
angle::Result OffscreenSurfaceVk::initializeContents(const gl::Context *context,
const gl::ImageIndex &imageIndex)
{
@@ -351,7 +359,7 @@
EGLNativeWindowType window,
EGLint width,
EGLint height)
- : SurfaceImpl(surfaceState),
+ : SurfaceVk(surfaceState),
mNativeWindowType(window),
mSurface(VK_NULL_HANDLE),
mInstance(VK_NULL_HANDLE),
@@ -364,10 +372,10 @@
mCurrentSwapchainImageIndex(0),
mCurrentSwapHistoryIndex(0)
{
- mDepthStencilRenderTarget.init(&mDepthStencilImage, &mDepthStencilImageView, 0, 0, nullptr);
// Initialize the color render target with the multisampled targets. If not multisampled, the
// render target will be updated to refer to a swapchain image on every acquire.
- mColorRenderTarget.init(&mColorImageMS, &mColorImageViewMS, 0, 0, nullptr);
+ mColorRenderTarget.init(&mColorImageMS, &mColorImageViewMS, 0, 0);
+ mDepthStencilRenderTarget.init(&mDepthStencilImage, &mDepthStencilImageView, 0, 0);
}
WindowSurfaceVk::~WindowSurfaceVk()
@@ -604,8 +612,7 @@
&mColorImageViewMS, 0, 1));
// Clear the image if it has emulated channels.
- ANGLE_TRY(
- mColorImageMS.clearIfEmulatedFormat(displayVk, gl::ImageIndex::Make2D(0), format));
+ mColorImageMS.clearIfEmulatedFormat(displayVk, gl::ImageIndex::Make2D(0), format);
}
mSwapchainImages.resize(imageCount);
@@ -627,8 +634,7 @@
// Clear the image if it has emulated channels. If a multisampled image exists, this
// image will be unused until a pre-present resolve, at which point it will be fully
// initialized and wouldn't need a clear.
- ANGLE_TRY(
- member.image.clearIfEmulatedFormat(displayVk, gl::ImageIndex::Make2D(0), format));
+ member.image.clearIfEmulatedFormat(displayVk, gl::ImageIndex::Make2D(0), format);
}
}
@@ -652,8 +658,7 @@
// We will need to pass depth/stencil image views to the RenderTargetVk in the future.
// Clear the image if it has emulated channels.
- ANGLE_TRY(mDepthStencilImage.clearIfEmulatedFormat(displayVk, gl::ImageIndex::Make2D(0),
- dsFormat));
+ mDepthStencilImage.clearIfEmulatedFormat(displayVk, gl::ImageIndex::Make2D(0), dsFormat);
}
return angle::Result::Continue;
@@ -880,7 +885,7 @@
// Update the swap history for this presentation
swap.sharedFence = renderer->getLastSubmittedFence();
- swap.semaphores = std::move(mFlushSemaphoreChain);
+ swap.semaphores = std::move(mFlushSemaphoreChain);
++mCurrentSwapHistoryIndex;
mCurrentSwapHistoryIndex =
mCurrentSwapHistoryIndex == mSwapHistory.size() ? 0 : mCurrentSwapHistoryIndex;
@@ -1053,24 +1058,6 @@
return EGL_BUFFER_DESTROYED;
}
-angle::Result WindowSurfaceVk::getAttachmentRenderTarget(const gl::Context *context,
- GLenum binding,
- const gl::ImageIndex &imageIndex,
- FramebufferAttachmentRenderTarget **rtOut)
-{
- if (binding == GL_BACK)
- {
- *rtOut = &mColorRenderTarget;
- }
- else
- {
- ASSERT(binding == GL_DEPTH || binding == GL_STENCIL || binding == GL_DEPTH_STENCIL);
- *rtOut = &mDepthStencilRenderTarget;
- }
-
- return angle::Result::Continue;
-}
-
angle::Result WindowSurfaceVk::getCurrentFramebuffer(vk::Context *context,
const vk::RenderPass &compatibleRenderPass,
vk::Framebuffer **framebufferOut)
diff --git a/src/libANGLE/renderer/vulkan/SurfaceVk.h b/src/libANGLE/renderer/vulkan/SurfaceVk.h
index ee37d1e..38ee62a 100644
--- a/src/libANGLE/renderer/vulkan/SurfaceVk.h
+++ b/src/libANGLE/renderer/vulkan/SurfaceVk.h
@@ -20,7 +20,23 @@
{
class RendererVk;
-class OffscreenSurfaceVk : public SurfaceImpl
+class SurfaceVk : public SurfaceImpl
+{
+ public:
+ angle::Result getAttachmentRenderTarget(const gl::Context *context,
+ GLenum binding,
+ const gl::ImageIndex &imageIndex,
+ FramebufferAttachmentRenderTarget **rtOut) override;
+
+ protected:
+ SurfaceVk(const egl::SurfaceState &surfaceState);
+ ~SurfaceVk() override;
+
+ RenderTargetVk mColorRenderTarget;
+ RenderTargetVk mDepthStencilRenderTarget;
+};
+
+class OffscreenSurfaceVk : public SurfaceVk
{
public:
OffscreenSurfaceVk(const egl::SurfaceState &surfaceState, EGLint width, EGLint height);
@@ -52,11 +68,6 @@
EGLint isPostSubBufferSupported() const override;
EGLint getSwapBehavior() const override;
- angle::Result getAttachmentRenderTarget(const gl::Context *context,
- GLenum binding,
- const gl::ImageIndex &imageIndex,
- FramebufferAttachmentRenderTarget **rtOut) override;
-
angle::Result initializeContents(const gl::Context *context,
const gl::ImageIndex &imageIndex) override;
@@ -77,7 +88,6 @@
vk::ImageHelper image;
vk::ImageView imageView;
- RenderTargetVk renderTarget;
};
angle::Result initializeImpl(DisplayVk *displayVk);
@@ -89,7 +99,7 @@
AttachmentImage mDepthStencilAttachment;
};
-class WindowSurfaceVk : public SurfaceImpl
+class WindowSurfaceVk : public SurfaceVk
{
public:
WindowSurfaceVk(const egl::SurfaceState &surfaceState,
@@ -125,11 +135,6 @@
EGLint isPostSubBufferSupported() const override;
EGLint getSwapBehavior() const override;
- angle::Result getAttachmentRenderTarget(const gl::Context *context,
- GLenum binding,
- const gl::ImageIndex &imageIndex,
- FramebufferAttachmentRenderTarget **rtOut) override;
-
angle::Result initializeContents(const gl::Context *context,
const gl::ImageIndex &imageIndex) override;
@@ -147,7 +152,7 @@
VkInstance mInstance;
private:
- virtual angle::Result createSurfaceVk(vk::Context *context, gl::Extents *extentsOut) = 0;
+ virtual angle::Result createSurfaceVk(vk::Context *context, gl::Extents *extentsOut) = 0;
virtual angle::Result getCurrentWindowSize(vk::Context *context, gl::Extents *extentsOut) = 0;
angle::Result initializeImpl(DisplayVk *displayVk);
@@ -178,9 +183,6 @@
VkSurfaceTransformFlagBitsKHR mPreTransform;
VkCompositeAlphaFlagBitsKHR mCompositeAlpha;
- RenderTargetVk mColorRenderTarget;
- RenderTargetVk mDepthStencilRenderTarget;
-
uint32_t mCurrentSwapchainImageIndex;
struct SwapchainImage : angle::NonCopyable
diff --git a/src/libANGLE/renderer/vulkan/TextureVk.cpp b/src/libANGLE/renderer/vulkan/TextureVk.cpp
index 7663f16..2069c1d 100644
--- a/src/libANGLE/renderer/vulkan/TextureVk.cpp
+++ b/src/libANGLE/renderer/vulkan/TextureVk.cpp
@@ -403,7 +403,6 @@
destOffset.y + clippedSourceArea.y - sourceArea.y, 0);
RenderTargetVk *colorReadRT = framebufferVk->getColorReadRenderTarget();
- ANGLE_TRY(colorReadRT->ensureImageInitialized(contextVk));
const vk::Format &srcFormat = colorReadRT->getImageFormat();
const vk::Format &destFormat = renderer->getFormat(internalFormat.sizedInternalFormat);
@@ -930,7 +929,7 @@
mImage->initStagingBuffer(renderer, format);
mRenderTarget.init(mImage, &mDrawBaseLevelImageView, getNativeImageLevel(0),
- getNativeImageLayer(0), this);
+ getNativeImageLayer(0));
// Force re-creation of cube map render targets next time they are needed
mCubeMapRenderTargets.clear();
@@ -1215,7 +1214,7 @@
vk::ImageView *imageView;
ANGLE_TRY(getLayerLevelDrawImageView(contextVk, cubeMapFaceIndex, 0, &imageView));
mCubeMapRenderTargets[cubeMapFaceIndex].init(mImage, imageView, getNativeImageLevel(0),
- getNativeImageLayer(cubeMapFaceIndex), this);
+ getNativeImageLayer(cubeMapFaceIndex));
}
return angle::Result::Continue;
}
@@ -1285,6 +1284,8 @@
mImage->stageSubresourceRobustClear(imageIndex, format.angleFormat());
+ // Note that we cannot ensure the image is initialized because we might be calling subImage
+ // on a non-complete cube map.
return angle::Result::Continue;
}
diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.cpp b/src/libANGLE/renderer/vulkan/vk_helpers.cpp
index bfcd5a9..ede812b 100644
--- a/src/libANGLE/renderer/vulkan/vk_helpers.cpp
+++ b/src/libANGLE/renderer/vulkan/vk_helpers.cpp
@@ -2150,17 +2150,14 @@
stageSubresourceClear(index, format, kEmulatedInitColorValue, kWebGLInitDepthStencilValue);
}
-angle::Result ImageHelper::clearIfEmulatedFormat(Context *context,
- const gl::ImageIndex &index,
- const Format &format)
+void ImageHelper::clearIfEmulatedFormat(Context *context,
+ const gl::ImageIndex &index,
+ const Format &format)
{
if (format.hasEmulatedImageChannels())
{
stageSubresourceEmulatedClear(index, format.angleFormat());
- ANGLE_TRY(flushAllStagedUpdates(context));
}
-
- return angle::Result::Continue;
}
void ImageHelper::stageSubresourceClear(const gl::ImageIndex &index,
diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.h b/src/libANGLE/renderer/vulkan/vk_helpers.h
index 985539f..254fb05 100644
--- a/src/libANGLE/renderer/vulkan/vk_helpers.h
+++ b/src/libANGLE/renderer/vulkan/vk_helpers.h
@@ -690,9 +690,7 @@
// If the image has emulated channels, we clear them once so as not to leave garbage on those
// channels.
- angle::Result clearIfEmulatedFormat(Context *context,
- const gl::ImageIndex &index,
- const Format &format);
+ void clearIfEmulatedFormat(Context *context, const gl::ImageIndex &index, const Format &format);
// This will use the underlying dynamic buffer to allocate some memory to be used as a src or
// dst.
diff --git a/src/tests/gl_tests/StateChangeTest.cpp b/src/tests/gl_tests/StateChangeTest.cpp
index 61dda32..2fbcd42 100644
--- a/src/tests/gl_tests/StateChangeTest.cpp
+++ b/src/tests/gl_tests/StateChangeTest.cpp
@@ -1267,6 +1267,9 @@
void simpleDrawWithBuffer(GLBuffer *buffer);
void simpleDrawWithColor(const GLColor &color);
+
+ using UpdateFunc = std::function<void(GLenum, GLTexture *, GLint, GLint, const GLColor &)>;
+ void updateTextureBoundToFramebufferHelper(UpdateFunc updateFunc);
};
class SimpleStateChangeTestES3 : public SimpleStateChangeTest
@@ -1789,6 +1792,95 @@
ASSERT_GL_NO_ERROR();
}
+void SimpleStateChangeTest::updateTextureBoundToFramebufferHelper(UpdateFunc updateFunc)
+{
+ std::vector<GLColor> red(4, GLColor::red);
+ std::vector<GLColor> green(4, GLColor::green);
+
+ GLTexture renderTarget;
+ glBindTexture(GL_TEXTURE_2D, renderTarget);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, red.data());
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, renderTarget,
+ 0);
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_DRAW_FRAMEBUFFER);
+ glViewport(0, 0, 2, 2);
+ ASSERT_GL_NO_ERROR();
+
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_2D, tex);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, red.data());
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ // Draw once to flush dirty state bits.
+ draw2DTexturedQuad(0.5f, 1.0f, true);
+
+ ASSERT_GL_NO_ERROR();
+
+ // Update the (0, 1) pixel to be blue
+ updateFunc(GL_TEXTURE_2D, &renderTarget, 0, 1, GLColor::blue);
+
+ // Draw green to the right half of the Framebuffer.
+ glBindTexture(GL_TEXTURE_2D, tex);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, green.data());
+ glViewport(1, 0, 1, 2);
+ draw2DTexturedQuad(0.5f, 1.0f, true);
+
+ // Update the (1, 1) pixel to be yellow
+ updateFunc(GL_TEXTURE_2D, &renderTarget, 1, 1, GLColor::yellow);
+
+ ASSERT_GL_NO_ERROR();
+
+ // Verify we have a quad with the right colors in the FBO.
+ std::vector<GLColor> expected = {
+ {GLColor::red, GLColor::green, GLColor::blue, GLColor::yellow}};
+ std::vector<GLColor> actual(4);
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
+ glReadPixels(0, 0, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, actual.data());
+ EXPECT_EQ(expected, actual);
+}
+
+// Tests that TexSubImage updates are flushed before rendering.
+TEST_P(SimpleStateChangeTest, TexSubImageOnTextureBoundToFrambuffer)
+{
+ auto updateFunc = [](GLenum textureBinding, GLTexture *tex, GLint x, GLint y,
+ const GLColor &color) {
+ glBindTexture(textureBinding, *tex);
+ glTexSubImage2D(textureBinding, 0, x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, color.data());
+ };
+
+ updateTextureBoundToFramebufferHelper(updateFunc);
+}
+
+// Tests that CopyTexSubImage updates are flushed before rendering.
+TEST_P(SimpleStateChangeTest, CopyTexSubImageOnTextureBoundToFrambuffer)
+{
+ GLTexture copySource;
+ glBindTexture(GL_TEXTURE_2D, copySource);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+ GLFramebuffer copyFBO;
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, copyFBO);
+ glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, copySource, 0);
+
+ ASSERT_GL_NO_ERROR();
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_READ_FRAMEBUFFER);
+
+ auto updateFunc = [©Source](GLenum textureBinding, GLTexture *tex, GLint x, GLint y,
+ const GLColor &color) {
+ glBindTexture(GL_TEXTURE_2D, copySource);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, color.data());
+
+ glBindTexture(textureBinding, *tex);
+ glCopyTexSubImage2D(textureBinding, 0, x, y, 0, 0, 1, 1);
+ };
+
+ updateTextureBoundToFramebufferHelper(updateFunc);
+}
+
// Tests deleting a Framebuffer that is in use.
TEST_P(SimpleStateChangeTest, DeleteFramebufferInUse)
{
diff --git a/src/tests/test_utils/ANGLETest.h b/src/tests/test_utils/ANGLETest.h
index 600c9bc..e92dee8 100644
--- a/src/tests/test_utils/ANGLETest.h
+++ b/src/tests/test_utils/ANGLETest.h
@@ -67,6 +67,11 @@
#define EXPECT_EGLENUM_EQ(expected, actual) \
EXPECT_EQ(static_cast<EGLenum>(expected), static_cast<EGLenum>(actual))
+#define ASSERT_GL_FRAMEBUFFER_COMPLETE(framebuffer) \
+ ASSERT_GLENUM_EQ(GL_FRAMEBUFFER_COMPLETE, glCheckFramebufferStatus(framebuffer))
+#define EXPECT_GL_FRAMEBUFFER_COMPLETE(framebuffer) \
+ EXPECT_GLENUM_EQ(GL_FRAMEBUFFER_COMPLETE, glCheckFramebufferStatus(framebuffer))
+
namespace angle
{
struct GLColorRGB
@@ -75,6 +80,9 @@
GLColorRGB(GLubyte r, GLubyte g, GLubyte b);
GLColorRGB(const angle::Vector3 &floatColor);
+ const GLubyte *data() const { return &R; }
+ GLubyte *data() { return &R; }
+
GLubyte R, G, B;
static const GLColorRGB black;