/* * Copyright (C) 2011, 2012 Research In Motion Limited. All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "config.h" #if USE(ACCELERATED_COMPOSITING) #include "LayerTiler.h" #include "BitmapImage.h" #include "LayerCompositingThread.h" #include "LayerMessage.h" #include "LayerWebKitThread.h" #include "NativeImageSkia.h" #include "TextureCacheCompositingThread.h" #include #include #include using namespace std; namespace WebCore { // This is used to make the viewport as used in texture visibility calculations // slightly larger so textures are uploaded before becoming really visible. const float viewportInflationFactor = 1.1f; // The tileMultiplier indicates how many tiles will fit in the largest dimension // of the screen, if drawn using identity transform. // We use half the screen size as tile size, to reduce the texture upload time // for small repaint rects. Compared to using screen size directly, this should // make most small invalidations 16x faster, unless they're unfortunate enough // to intersect two or more tiles, where it would be 8x-1x faster. const int tileMultiplier = 4; static void transformPoint(float x, float y, const TransformationMatrix& m, float* result) { // Squash the Z coordinate so that layers aren't clipped against the near and // far plane. Note that the perspective is maintained as we're still passing // down the W coordinate. result[0] = x * m.m11() + y * m.m21() + m.m41(); result[1] = x * m.m12() + y * m.m22() + m.m42(); result[2] = 0; result[3] = x * m.m14() + y * m.m24() + m.m44(); } static IntSize defaultTileSize() { static IntSize screenSize = BlackBerry::Platform::Graphics::Screen::primaryScreen()->nativeSize(); static int dim = max(screenSize.width(), screenSize.height()) / tileMultiplier; return IntSize(dim, dim); } LayerTiler::LayerTiler(LayerWebKitThread* layer) : m_layer(layer) , m_tilingDisabled(false) , m_contentsDirty(false) , m_tileSize(defaultTileSize()) , m_clearTextureJobs(false) , m_hasMissingTextures(false) , m_contentsScale(0.0) { ref(); // This ref() is matched by a deref in layerCompositingThreadDestroyed(); } LayerTiler::~LayerTiler() { // Someone should have called LayerTiler::deleteTextures() // before now. We can't call it here because we have no // OpenGL context. ASSERT(m_tiles.isEmpty()); } void LayerTiler::layerWebKitThreadDestroyed() { m_layer = 0; } void LayerTiler::layerCompositingThreadDestroyed(LayerCompositingThread*) { ASSERT(isCompositingThread()); deref(); // Matched by ref() in constructor; } void LayerTiler::setNeedsDisplay(const FloatRect& dirtyRect) { m_dirtyRect.unite(dirtyRect); m_contentsDirty = true; } void LayerTiler::setNeedsDisplay() { m_dirtyRect.setLocation(FloatPoint::zero()); m_dirtyRect.setSize(m_layer->bounds()); m_contentsDirty = true; } void LayerTiler::updateTextureContentsIfNeeded(double scale) { updateTileSize(); HashSet renderJobs; { MutexLocker locker(m_renderJobsMutex); if (!m_contentsDirty && m_renderJobs.isEmpty()) return; renderJobs = m_renderJobs; } // There's no point in drawing contents at a higher resolution for scale // invariant layers. if (m_layer->sizeIsScaleInvariant()) scale = 1.0; bool isZoomJob = false; if (scale != m_contentsScale) { // The first time around, it does not count as a zoom job. if (m_contentsScale) isZoomJob = true; m_contentsScale = scale; } IntRect dirtyRect = enclosingIntRect(m_dirtyRect); IntSize requiredTextureSize; if (m_layer->drawsContent()) { // Layer contents must be drawn into a canvas. IntRect untransformedDirtyRect(dirtyRect); IntRect boundsRect(IntPoint::zero(), m_layer->bounds()); IntRect untransformedBoundsRect(boundsRect); requiredTextureSize = boundsRect.size(); if (scale != 1.0) { TransformationMatrix matrix; matrix.scale(scale); dirtyRect = matrix.mapRect(untransformedDirtyRect); requiredTextureSize = matrix.mapRect(IntRect(IntPoint::zero(), requiredTextureSize)).size(); boundsRect = matrix.mapRect(untransformedBoundsRect); } if (requiredTextureSize != m_pendingTextureSize) dirtyRect = boundsRect; else { // Clip the dirtyRect to the size of the layer to avoid drawing // outside the bounds of the backing texture. dirtyRect.intersect(boundsRect); } } else if (m_layer->contents()) { // Layer is a container, and it contains an Image. requiredTextureSize = m_layer->contents()->size(); dirtyRect = IntRect(IntPoint::zero(), requiredTextureSize); } // If the new size is empty, clear the visibility jobs if (requiredTextureSize.isEmpty() && renderJobs.size()) { renderJobs.clear(); MutexLocker locker(m_renderJobsMutex); m_renderJobs.clear(); } // If we need display because we no longer need to be displayed, due to texture size becoming 0 x 0, // or if we're re-rendering the whole thing anyway, clear old texture jobs. if (requiredTextureSize.isEmpty() || dirtyRect == IntRect(IntPoint::zero(), requiredTextureSize)) clearTextureJobs(); HashSet finishedJobs; if (!renderJobs.isEmpty()) { if (Image* image = m_layer->contents()) { bool isOpaque = false; if (image->isBitmapImage()) isOpaque = !static_cast(image)->currentFrameHasAlpha(); if (NativeImagePtr nativeImage = image->nativeImageForCurrentFrame()) { SkBitmap bitmap = SkBitmap(nativeImage->bitmap()); addTextureJob(TextureJob::setContents(bitmap, isOpaque)); } } else { // There might still be some pending render jobs due to visibility changes. for (HashSet::iterator it = renderJobs.begin(); it != renderJobs.end(); ++it) { { // Check if the job has been cancelled. MutexLocker locker(m_renderJobsMutex); if (!m_renderJobs.contains(*it)) continue; m_renderJobs.remove(*it); } IntRect tileRect = rectForTile(*it, requiredTextureSize); if (tileRect.isEmpty()) continue; bool isSolidColor = false; Color color; SkBitmap bitmap = m_layer->paintContents(tileRect, scale, &isSolidColor, &color); // bitmap can be null here. Make requiredTextureSize empty so that we // will not try to update and draw the layer. if (!bitmap.isNull()) { if (isSolidColor) addTextureJob(TextureJob::setContentsToColor(color, *it)); else addTextureJob(TextureJob::updateContents(bitmap, tileRect, m_layer->isOpaque())); } finishedJobs.add(*it); } } } bool didResize = false; IntRect previousTextureRect(IntPoint::zero(), m_pendingTextureSize); if (m_pendingTextureSize != requiredTextureSize) { didResize = true; m_pendingTextureSize = requiredTextureSize; addTextureJob(TextureJob::resizeContents(m_pendingTextureSize)); } m_contentsDirty = false; m_dirtyRect = FloatRect(); if (dirtyRect.isEmpty() || requiredTextureSize.isEmpty()) return; if (Image* image = m_layer->contents()) { bool isOpaque = false; if (image->isBitmapImage()) isOpaque = !static_cast(image)->currentFrameHasAlpha(); // No point in tiling an image layer, the image is already stored as an SkBitmap NativeImagePtr nativeImage = m_layer->contents()->nativeImageForCurrentFrame(); if (nativeImage) { SkBitmap bitmap = SkBitmap(nativeImage->bitmap()); addTextureJob(TextureJob::setContents(bitmap, isOpaque)); } return; } IntPoint topLeft = dirtyRect.minXMinYCorner(); IntPoint bottomRight = dirtyRect.maxXMaxYCorner(); // This is actually a pixel below and to the right of the dirtyRect. IntSize tileMaximumSize(tileSize()); IntRect rectForOneTile(IntPoint::zero(), tileMaximumSize); bool wasOneTile = rectForOneTile.contains(previousTextureRect); bool isOneTile = rectForOneTile.contains(IntRect(IntPoint::zero(), m_pendingTextureSize)); IntPoint origin = originOfTile(indexOfTile(topLeft)); IntRect tileRect; for (tileRect.setX(origin.x()); tileRect.x() < bottomRight.x(); tileRect.setX(tileRect.x() + tileMaximumSize.width())) { for (tileRect.setY(origin.y()); tileRect.y() < bottomRight.y(); tileRect.setY(tileRect.y() + tileMaximumSize.height())) { tileRect.setWidth(min(requiredTextureSize.width() - tileRect.x(), tileMaximumSize.width())); tileRect.setHeight(min(requiredTextureSize.height() - tileRect.y(), tileMaximumSize.height())); IntRect localDirtyRect(dirtyRect); localDirtyRect.intersect(tileRect); if (localDirtyRect.isEmpty()) continue; TileIndex index = indexOfTile(tileRect.location()); // If we already did this as part of one of the render jobs due to // visibility changes, don't render that tile again. if (finishedJobs.contains(index)) continue; if (!shouldPerformRenderJob(index, !isZoomJob)) { // Avoid checkerboarding unless the layer is resized. When // resized, the contents are likely to change appearance, for // example due to aspect ratio change. However, if it is a // resize due to zooming, the aspect ratio and content will // stay the same, and we can keep the old texture content as a // preview. // FIXME: the zoom preview only works if we don't re-tile the // layer. We need to store texture coordinates in // WebCore::Texture to be able to fix that. if (didResize && !(isZoomJob && wasOneTile && isOneTile)) addTextureJob(TextureJob::discardContents(tileRect)); else addTextureJob(TextureJob::dirtyContents(tileRect)); continue; } // Just in case a new job for this tile has just been inserted by compositing thread. removeRenderJob(index); // FIXME: We are always drawing whole tiles at the moment, because // we currently can't keep track of which part of a tile is // rendered and which is not. Sending only a subrectangle of a tile // to the compositing thread might cause it to be uploaded using // glTexImage, if the texture was previously evicted from the cache. // The result would be that a subrectangle of the tile was stretched // to fit the tile geometry, appearing as a glaring misrendering of // the web page. bool isSolidColor = false; Color color; SkBitmap bitmap = m_layer->paintContents(tileRect, scale, &isSolidColor, &color); // bitmap can be null here. Make requiredTextureSize empty so that we // will not try to update and draw the layer. if (!bitmap.isNull()) { if (isSolidColor) addTextureJob(TextureJob::setContentsToColor(color, index)); else addTextureJob(TextureJob::updateContents(bitmap, tileRect, m_layer->isOpaque())); } } } } bool LayerTiler::shouldPerformRenderJob(const TileIndex& index, bool allowPrefill) { // Tiles that are not currently visible on the compositing thread may still // deserve to be rendered if they should be prefilled... if (allowPrefill && shouldPrefillTile(index)) return true; // Or if they are visible according to the state that's about to be // committed. We do a visibility test using the current transform state. IntRect contentRect = rectForTile(index, m_pendingTextureSize); return m_layer->contentsVisible(LayerWebKitThread::mapFromTransformed(contentRect, m_contentsScale)); } void LayerTiler::addTextureJob(const TextureJob& job) { m_pendingTextureJobs.append(job); } void LayerTiler::clearTextureJobs() { // Clear any committed texture jobs on next invocation of LayerTiler::commitPendingTextureUploads(). m_clearTextureJobs = true; removeUpdateContentsJobs(m_pendingTextureJobs); } void LayerTiler::commitPendingTextureUploads() { if (m_clearTextureJobs) { removeUpdateContentsJobs(m_textureJobs); m_clearTextureJobs = false; } for (Vector::iterator it = m_pendingTextureJobs.begin(); it != m_pendingTextureJobs.end(); ++it) m_textureJobs.append(*it); m_pendingTextureJobs.clear(); } void LayerTiler::layerVisibilityChanged(LayerCompositingThread*, bool visible) { // For visible layers, we handle the tile-level visibility // in the draw loop, see LayerTiler::drawTextures(). if (visible) return; { // All tiles are invisible now. MutexLocker locker(m_renderJobsMutex); m_renderJobs.clear(); } for (TileMap::iterator it = m_tiles.begin(); it != m_tiles.end(); ++it) { TileIndex index = (*it).key; LayerTile* tile = (*it).value; tile->setVisible(false); } } void LayerTiler::uploadTexturesIfNeeded(LayerCompositingThread*) { TileJobsMap tileJobsMap; Deque::const_iterator textureJobIterEnd = m_textureJobs.end(); for (Deque::const_iterator textureJobIter = m_textureJobs.begin(); textureJobIter != textureJobIterEnd; ++textureJobIter) processTextureJob(*textureJobIter, tileJobsMap); TileJobsMap::const_iterator tileJobsIterEnd = tileJobsMap.end(); for (TileJobsMap::const_iterator tileJobsIter = tileJobsMap.begin(); tileJobsIter != tileJobsIterEnd; ++tileJobsIter) { IntPoint origin = originOfTile(tileJobsIter->key); LayerTile* tile = m_tiles.get(tileJobsIter->key); if (!tile) { if (origin.x() >= m_requiredTextureSize.width() || origin.y() >= m_requiredTextureSize.height()) continue; tile = new LayerTile(); m_tiles.add(tileJobsIter->key, tile); } IntRect tileRect(origin, tileSize()); tileRect.setWidth(min(m_requiredTextureSize.width() - tileRect.x(), tileRect.width())); tileRect.setHeight(min(m_requiredTextureSize.height() - tileRect.y(), tileRect.height())); performTileJob(tile, *tileJobsIter->value, tileRect); } m_textureJobs.clear(); } void LayerTiler::processTextureJob(const TextureJob& job, TileJobsMap& tileJobsMap) { if (job.m_type == TextureJob::ResizeContents) { IntSize pendingTextureSize = job.m_dirtyRect.size(); if (pendingTextureSize.width() < m_requiredTextureSize.width() || pendingTextureSize.height() < m_requiredTextureSize.height()) pruneTextures(); m_requiredTextureSize = pendingTextureSize; return; } if (job.m_type == TextureJob::SetContentsToColor) { addTileJob(job.m_index, job, tileJobsMap); return; } IntSize tileMaximumSize = tileSize(); IntPoint topLeft = job.m_dirtyRect.minXMinYCorner(); IntPoint bottomRight = job.m_dirtyRect.maxXMaxYCorner(); // This is actually a pixel below and to the right of the dirtyRect. IntPoint origin = originOfTile(indexOfTile(topLeft)); IntRect tileRect; for (tileRect.setX(origin.x()); tileRect.x() < bottomRight.x(); tileRect.setX(tileRect.x() + tileMaximumSize.width())) { for (tileRect.setY(origin.y()); tileRect.y() < bottomRight.y(); tileRect.setY(tileRect.y() + tileMaximumSize.height())) addTileJob(indexOfTile(tileRect.location()), job, tileJobsMap); } } void LayerTiler::addTileJob(const TileIndex& index, const TextureJob& job, TileJobsMap& tileJobsMap) { // HashMap::add always returns a valid iterator even the key already exists. TileJobsMap::AddResult result = tileJobsMap.add(index, &job); // Successfully added the new job. if (result.isNewEntry) return; // In this case we leave the previous job. if (job.m_type == TextureJob::DirtyContents && result.iterator->value->m_type == TextureJob::DiscardContents) return; // Override the previous job. result.iterator->value = &job; } void LayerTiler::performTileJob(LayerTile* tile, const TextureJob& job, const IntRect& tileRect) { switch (job.m_type) { case TextureJob::SetContentsToColor: tile->setContentsToColor(job.m_color); return; case TextureJob::SetContents: tile->setContents(job.m_contents, tileRect, indexOfTile(tileRect.location()), job.m_isOpaque); return; case TextureJob::UpdateContents: tile->updateContents(job.m_contents, job.m_dirtyRect, tileRect, job.m_isOpaque); return; case TextureJob::DiscardContents: tile->discardContents(); return; case TextureJob::DirtyContents: tile->setContentsDirty(); return; case TextureJob::Unknown: case TextureJob::ResizeContents: ASSERT_NOT_REACHED(); return; } ASSERT_NOT_REACHED(); } void LayerTiler::drawTextures(LayerCompositingThread* layer, double scale, int pos, int texCoord) { drawTexturesInternal(layer, scale, pos, texCoord, false /* drawMissing */); } void LayerTiler::drawMissingTextures(LayerCompositingThread* layer, double scale, int pos, int texCoord) { drawTexturesInternal(layer, scale, pos, texCoord, true /* drawMissing */); } void LayerTiler::drawTexturesInternal(LayerCompositingThread* layer, double scale, int positionLocation, int texCoordLocation, bool drawMissing) { const TransformationMatrix& drawTransform = layer->drawTransform(); FloatSize bounds = layer->bounds(); if (layer->sizeIsScaleInvariant()) { bounds.setWidth(bounds.width() / scale); bounds.setHeight(bounds.width() / scale); } float texcoords[4 * 2] = { 0, 0, 0, 1, 1, 1, 1, 0 }; float vertices[4 * 4]; glVertexAttribPointer(positionLocation, 4, GL_FLOAT, GL_FALSE, 0, vertices); glVertexAttribPointer(texCoordLocation, 2, GL_FLOAT, GL_FALSE, 0, texcoords); m_hasMissingTextures = false; int maxw = tileSize().width(); int maxh = tileSize().height(); float sx = static_cast(bounds.width()) / m_requiredTextureSize.width(); float sy = static_cast(bounds.height()) / m_requiredTextureSize.height(); bool needsDisplay = false; bool blending = !drawMissing; IntRect tileRect; for (tileRect.setX(0); tileRect.x() < m_requiredTextureSize.width(); tileRect.setX(tileRect.x() + maxw)) { for (tileRect.setY(0); tileRect.y() < m_requiredTextureSize.height(); tileRect.setY(tileRect.y() + maxh)) { TileIndex index = indexOfTile(tileRect.location()); LayerTile* tile = m_tiles.get(index); if (!tile) { tile = new LayerTile(); m_tiles.add(index, tile); } float x = index.i() * maxw * sx; float y = index.j() * maxh * sy; float w = min(bounds.width() - x, maxw * sx); float h = min(bounds.height() - y, maxh * sy); float ox = x - bounds.width() / 2.0; float oy = y - bounds.height() / 2.0; // We apply the transformation by hand, since we need the z coordinate // as well (to do perspective correct texturing) and we don't need // to divide by w by hand, the GPU will do that for us transformPoint(ox, oy, drawTransform, &vertices[0]); transformPoint(ox, oy + h, drawTransform, &vertices[4]); transformPoint(ox + w, oy + h, drawTransform, &vertices[8]); transformPoint(ox + w, oy, drawTransform, &vertices[12]); // Inflate the rect somewhat to attempt to make textures render before they show // up on screen. float d = viewportInflationFactor; FloatRect rect(-d, -d, 2 * d, 2 * d); FloatQuad quad(FloatPoint(vertices[0] / vertices[3], vertices[1] / vertices[3]), FloatPoint(vertices[4] / vertices[7], vertices[5] / vertices[7]), FloatPoint(vertices[8] / vertices[11], vertices[9] / vertices[11]), FloatPoint(vertices[12] / vertices[15], vertices[13] / vertices[15])); bool visible = quad.boundingBox().intersects(rect); bool wasVisible = tile->isVisible(); tile->setVisible(visible); // This method is called in two passes. The first pass draws all // visible tiles with textures. // If a visible tile has no texture, set the m_hasMissingTextures // flag, to indicate that we need a second pass. // The second "drawMissing" pass draws all visible tiles without // textures as checkerboard. // However, don't draw brand new tiles as checkerboard. The checker- // board indicates that a tile has dirty contents, but that's not // the case if it's brand new. if (visible) { bool hasTexture = tile->hasTexture(); if (!hasTexture) m_hasMissingTextures = true; if (hasTexture && !drawMissing) { Texture* texture = tile->texture(); if (texture->isOpaque() && layer->drawOpacity() == 1.0f && !layer->maskLayer()) { if (blending) { blending = false; glDisable(GL_BLEND); } } else if (!blending) { blending = true; glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); } textureCacheCompositingThread()->textureAccessed(texture); glBindTexture(GL_TEXTURE_2D, texture->textureId()); } if (hasTexture != drawMissing) glDrawArrays(GL_TRIANGLE_FAN, 0, 4); if (tile->isDirty()) { addRenderJob(index); needsDisplay = true; } } else if (wasVisible) removeRenderJob(index); } } // Return early for the drawMissing case, don't flag us as needing commit. if (drawMissing) return; // Switch on blending again (we know that drawMissing == false). if (!blending) { glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); } // If we schedule a commit, visibility will be updated, and display will // happen if there are any visible and dirty textures. if (needsDisplay) layer->setNeedsCommit(); } void LayerTiler::addRenderJob(const TileIndex& index) { ASSERT(isCompositingThread()); MutexLocker locker(m_renderJobsMutex); m_renderJobs.add(index); } void LayerTiler::removeRenderJob(const TileIndex& index) { MutexLocker locker(m_renderJobsMutex); m_renderJobs.remove(index); } void LayerTiler::deleteTextures(LayerCompositingThread*) { // Since textures are deleted by a synchronous message // from WebKit thread to compositing thread, we don't need // any synchronization mechanism here, even though we are // touching some WebKit thread state. if (m_tiles.size()) { for (TileMap::iterator it = m_tiles.begin(); it != m_tiles.end(); ++it) (*it).value->discardContents(); m_tiles.clear(); m_contentsDirty = true; } // For various reasons, e.g. page cache, someone may try // to render us after the textures were deleted. m_pendingTextureSize = IntSize(); m_requiredTextureSize = IntSize(); } void LayerTiler::pruneTextures() { // Prune tiles that are no longer needed. Vector tilesToDelete; for (TileMap::iterator it = m_tiles.begin(); it != m_tiles.end(); ++it) { TileIndex index = (*it).key; IntPoint origin = originOfTile(index); if (origin.x() >= m_requiredTextureSize.width() || origin.y() >= m_requiredTextureSize.height()) tilesToDelete.append(index); } for (Vector::iterator it = tilesToDelete.begin(); it != tilesToDelete.end(); ++it) { LayerTile* tile = m_tiles.take(*it); tile->discardContents(); delete tile; } } void LayerTiler::updateTileSize() { IntSize size = m_tilingDisabled ? m_layer->bounds() : defaultTileSize(); const IntSize maxTextureSize(2048, 2048); size = size.shrunkTo(maxTextureSize); if (m_tileSize == size || size.isEmpty()) return; // Invalidate the whole layer if tile size changes. setNeedsDisplay(); m_tileSize = size; } void LayerTiler::disableTiling(bool disable) { if (m_tilingDisabled == disable) return; m_tilingDisabled = disable; updateTileSize(); } void LayerTiler::scheduleCommit() { ASSERT(isWebKitThread()); if (m_layer) m_layer->setNeedsCommit(); } bool LayerTiler::shouldPrefillTile(const TileIndex& index) { IntRect prefillTargetRect = BlackBerry::Platform::Settings::instance()->layerTilerPrefillRect(); IntRect tileRect = IntRect(originOfTile(index), tileSize()); return prefillTargetRect.intersects(tileRect); } TileIndex LayerTiler::indexOfTile(const WebCore::IntPoint& origin) { int offsetX = origin.x(); int offsetY = origin.y(); if (offsetX) offsetX = offsetX / tileSize().width(); if (offsetY) offsetY = offsetY / tileSize().height(); return TileIndex(offsetX, offsetY); } IntPoint LayerTiler::originOfTile(const TileIndex& index) { return IntPoint(index.i() * tileSize().width(), index.j() * tileSize().height()); } IntRect LayerTiler::rectForTile(const TileIndex& index, const IntSize& bounds) { IntPoint origin = originOfTile(index); IntSize offset(origin.x(), origin.y()); IntSize size = tileSize().shrunkTo(bounds - offset); return IntRect(origin, size); } void LayerTiler::bindContentsTexture(LayerCompositingThread*) { ASSERT(m_tiles.size() == 1); if (m_tiles.size() != 1) return; const LayerTile* tile = m_tiles.begin()->value; ASSERT(tile->hasTexture()); if (!tile->hasTexture()) return; glBindTexture(GL_TEXTURE_2D, tile->texture()->textureId()); } } // namespace WebCore #endif