From 622f3bf11bebde5c73b5298ad3b1672685e887b1 Mon Sep 17 00:00:00 2001 From: "roc+@cs.cmu.edu" Date: Tue, 12 Jun 2007 13:56:04 -0700 Subject: [PATCH] Bug 384100. Implement word-based textrun cache. r=vlad --- gfx/thebes/public/Makefile.in | 1 + gfx/thebes/public/gfxFont.h | 36 +- gfx/thebes/public/gfxTextRunWordCache.h | 145 ++++++++ gfx/thebes/src/Makefile.in | 1 + gfx/thebes/src/gfxFont.cpp | 186 +++++++--- gfx/thebes/src/gfxSkipChars.cpp | 9 +- gfx/thebes/src/gfxTextRunWordCache.cpp | 469 ++++++++++++++++++++++++ 7 files changed, 792 insertions(+), 55 deletions(-) create mode 100644 gfx/thebes/public/gfxTextRunWordCache.h create mode 100644 gfx/thebes/src/gfxTextRunWordCache.cpp diff --git a/gfx/thebes/public/Makefile.in b/gfx/thebes/public/Makefile.in index fd267ce89c29..46719562b820 100644 --- a/gfx/thebes/public/Makefile.in +++ b/gfx/thebes/public/Makefile.in @@ -25,6 +25,7 @@ EXPORTS = gfxASurface.h \ gfxSkipChars.h \ gfxTypes.h \ gfxTextRunCache.h \ + gfxTextRunWordCache.h \ $(NULL) EXPORTS += gfxFontTest.h diff --git a/gfx/thebes/public/gfxFont.h b/gfx/thebes/public/gfxFont.h index 9d77700022a8..0a73e5c3ca51 100644 --- a/gfx/thebes/public/gfxFont.h +++ b/gfx/thebes/public/gfxFont.h @@ -430,6 +430,10 @@ public: // Flags in the mask 0x0000F000 are reserved for per-platform fonts // Flags in the mask 0x00000FFF are set by the textrun creator. enum { + USER_TEXT_FLAGS = 0xFFFF0000, + PLATFORM_TEXT_FLAGS = 0x0000F000, + TEXTRUN_TEXT_FLAGS = 0x00000FFF, + /** * When set, the text string pointer used to create the text run * is guaranteed to be available during the lifetime of the text run. @@ -789,6 +793,16 @@ public: void *GetUserData() const { return mUserData; } void SetUserData(void *aUserData) { mUserData = aUserData; } PRUint32 GetFlags() const { return mFlags; } + void SetFlagBits(PRUint32 aFlags) { + NS_ASSERTION(!(aFlags & ~gfxTextRunFactory::USER_TEXT_FLAGS), + "Only user flags should be mutable"); + mFlags |= aFlags; + } + void ClearFlagBits(PRUint32 aFlags) { + NS_ASSERTION(!(aFlags & ~gfxTextRunFactory::USER_TEXT_FLAGS), + "Only user flags should be mutable"); + mFlags &= ~aFlags; + } const gfxSkipChars& GetSkipChars() const { return mSkipChars; } PRUint32 GetAppUnitsPerDevUnit() const { return mAppUnitsPerDevUnit; } gfxFontGroup *GetFontGroup() const { return mFontGroup; } @@ -796,6 +810,11 @@ public: { return (mFlags & gfxTextRunFactory::TEXT_IS_8BIT) ? mText.mSingle : nsnull; } const PRUnichar *GetTextUnicode() const { return (mFlags & gfxTextRunFactory::TEXT_IS_8BIT) ? nsnull : mText.mDouble; } + const void *GetTextAt(PRUint32 aIndex) { + return (mFlags & gfxTextRunFactory::TEXT_IS_8BIT) + ? NS_STATIC_CAST(const void *, mText.mSingle + aIndex) + : NS_STATIC_CAST(const void *, mText.mDouble + aIndex); + } const PRUnichar GetChar(PRUint32 i) const { return (mFlags & gfxTextRunFactory::TEXT_IS_8BIT) ? mText.mSingle[i] : mText.mDouble[i]; } PRUint32 GetHashCode() const { return mHashCode; } @@ -1020,6 +1039,7 @@ public: void SetDetailedGlyphs(PRUint32 aCharIndex, const DetailedGlyph *aGlyphs, PRUint32 aNumGlyphs); void SetMissingGlyph(PRUint32 aCharIndex, PRUnichar aChar); + void SetSpaceGlyph(gfxFont *aFont, gfxContext *aContext, PRUint32 aCharIndex); // API for access to the raw glyph data, needed by gfxFont::Draw // and gfxFont::GetBoundingBox @@ -1034,6 +1054,16 @@ public: *aNumGlyphRuns = mGlyphRuns.Length(); return mGlyphRuns.Elements(); } + // Returns the index of the GlyphRun containing the given offset. + // Returns mGlyphRuns.Length() when aOffset is mCharacterCount. + PRUint32 FindFirstGlyphRunContaining(PRUint32 aOffset); + // Copy glyph data for a range of characters from aSource to this + // textrun. If aStealData is true then we actually steal the glyph data, + // setting the data in aSource to "missing". aDest should be in the last + // glyphrun. + virtual void CopyGlyphDataFrom(gfxTextRun *aSource, PRUint32 aStart, + PRUint32 aLength, PRUint32 aDest, + PRBool aStealData); nsExpirationState *GetExpirationState() { return &mExpirationState; } @@ -1042,9 +1072,6 @@ private: // Allocate aCount DetailedGlyphs for the given index DetailedGlyph *AllocateDetailedGlyphs(PRUint32 aCharIndex, PRUint32 aCount); - // Returns the index of the GlyphRun containing the given offset. - // Returns mGlyphRuns.Length() when aOffset is mCharacterCount. - PRUint32 FindFirstGlyphRunContaining(PRUint32 aOffset); // Computes the x-advance for a given cluster starting at aClusterOffset. Does // not include any spacing. Result is in appunits. PRInt32 ComputeClusterAdvance(PRUint32 aClusterOffset); @@ -1140,7 +1167,8 @@ public: virtual gfxFontGroup *Copy(const gfxFontStyle *aStyle) = 0; /** - * Tabs, CRs and LFs should be zero-width and invisible. + * Tabs, CRs and LFs should be zero-width and invisible. They should + * break up shaping. */ static PRBool IsInvisibleChar(PRUnichar ch) { return ch == '\t' || ch == '\r' || ch == '\n'; diff --git a/gfx/thebes/public/gfxTextRunWordCache.h b/gfx/thebes/public/gfxTextRunWordCache.h new file mode 100644 index 000000000000..caabc8144059 --- /dev/null +++ b/gfx/thebes/public/gfxTextRunWordCache.h @@ -0,0 +1,145 @@ +/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Foundation code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Vladimir Vukicevic + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#ifndef GFX_TEXT_RUN_WORD_CACHE_H +#define GFX_TEXT_RUN_WORD_CACHE_H + +#include "gfxFont.h" + +/** + * Cache individual "words" (strings delimited by white-space or white-space-like + * characters that don't involve kerning or ligatures) in textruns. + */ +class THEBES_API gfxTextRunWordCache { +public: + gfxTextRunWordCache() { + mCache.Init(100); + } + ~gfxTextRunWordCache() { + NS_ASSERTION(mCache.Count() == 0, "Textrun cache not empty!"); + } + + /** + * Create a textrun using cached words. + * @param aFlags the flags TEXT_IS_ASCII and TEXT_HAS_SURROGATES must be set + * by the caller, if applicable + * @param aIsInCache if true is returned, then RemoveTextRun must be called + * before the textrun changes or dies. + */ + gfxTextRun *MakeTextRun(const PRUnichar *aText, PRUint32 aLength, + gfxFontGroup *aFontGroup, + const gfxFontGroup::Parameters *aParams, + PRUint32 aFlags, PRBool *aIsInCache); + /** + * Create a textrun using cached words + * @param aFlags the flag TEXT_IS_ASCII must be set by the caller, + * if applicable + * @param aIsInCache if true is returned, then RemoveTextRun must be called + * before the textrun changes or dies. + */ + gfxTextRun *MakeTextRun(const PRUint8 *aText, PRUint32 aLength, + gfxFontGroup *aFontGroup, + const gfxFontGroup::Parameters *aParams, + PRUint32 aFlags, PRBool *aIsInCache); + + /** + * Remove a textrun from the cache. This must be called before aTextRun + * is deleted! The text in the textrun must still be valid. + */ + void RemoveTextRun(gfxTextRun *aTextRun); + +protected: + struct THEBES_API CacheHashKey { + void *mFontOrGroup; + const void *mString; + PRUint32 mLength; + PRUint32 mAppUnitsPerDevUnit; + PRUint32 mStringHash; + PRPackedBool mIsDoubleByteText; + }; + + class THEBES_API CacheHashEntry : public PLDHashEntryHdr { + public: + typedef const CacheHashKey &KeyType; + typedef const CacheHashKey *KeyTypePointer; + + // When constructing a new entry in the hashtable, the caller of Put() + // will fill us in. + CacheHashEntry(KeyTypePointer aKey) : mTextRun(nsnull), mWordOffset(0), + mHashedByFont(PR_FALSE) { } + CacheHashEntry(const CacheHashEntry& toCopy) { NS_ERROR("Should not be called"); } + ~CacheHashEntry() { } + + PRBool KeyEquals(const KeyTypePointer aKey) const; + static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; } + static PLDHashNumber HashKey(const KeyTypePointer aKey); + enum { ALLOW_MEMMOVE = PR_TRUE }; + + gfxTextRun *mTextRun; + // The offset of the start of the word in the textrun. The length of + // the word is not stored here because we can figure it out by + // looking at the textrun's text. + PRUint32 mWordOffset:31; + // This is set to true when the cache entry was hashed by the first + // font in mTextRun's fontgroup; it's false when the cache entry + // was hashed by the fontgroup itself. + PRUint32 mHashedByFont:1; + }; + + // Used to track words that should be copied from one textrun to + // another during the textrun construction process + struct DeferredWord { + gfxTextRun *mSourceTextRun; + PRUint32 mSourceOffset; + PRUint32 mDestOffset; + PRUint32 mLength; + PRUint32 mHash; + }; + + PRBool LookupWord(gfxTextRun *aTextRun, gfxFont *aFirstFont, + PRUint32 aStart, PRUint32 aEnd, PRUint32 aHash, + nsTArray* aDeferredWords); + void FinishTextRun(gfxTextRun *aTextRun, gfxTextRun *aNewRun, + const nsTArray& aDeferredWords, + PRBool aSuccessful); + void RemoveWord(gfxTextRun *aTextRun, PRUint32 aStart, + PRUint32 aEnd, PRUint32 aHash); + + nsTHashtable mCache; +}; + +#endif /* GFX_TEXT_RUN_WORD_CACHE_H */ diff --git a/gfx/thebes/src/Makefile.in b/gfx/thebes/src/Makefile.in index 29dcb7f9202e..6ce0093d9ef2 100644 --- a/gfx/thebes/src/Makefile.in +++ b/gfx/thebes/src/Makefile.in @@ -35,6 +35,7 @@ CPPSRCS = \ gfxRect.cpp \ gfxSkipChars.cpp \ gfxTextRunCache.cpp \ + gfxTextRunWordCache.cpp \ $(NULL) ifdef MOZ_TREE_CAIRO diff --git a/gfx/thebes/src/gfxFont.cpp b/gfx/thebes/src/gfxFont.cpp index ed378126405a..03d902275d43 100644 --- a/gfx/thebes/src/gfxFont.cpp +++ b/gfx/thebes/src/gfxFont.cpp @@ -531,24 +531,13 @@ gfxFontGroup::MakeSpaceTextRun(const Parameters *aParams, PRUint32 aFlags) aFlags |= TEXT_IS_8BIT | TEXT_IS_ASCII | TEXT_IS_PERSISTENT; static const PRUint8 space = ' '; - gfxFont *font = GetFontAt(0); - PRUint32 spaceGlyph = font->GetSpaceGlyph(); - float spaceWidth = font->GetMetrics().spaceWidth; - PRUint32 spaceWidthAppUnits = NS_lroundf(spaceWidth*aParams->mAppUnitsPerDevUnit); - if (!spaceGlyph || - !gfxTextRun::CompressedGlyph::IsSimpleGlyphID(spaceGlyph) || - !gfxTextRun::CompressedGlyph::IsSimpleAdvance(spaceWidthAppUnits)) - return MakeTextRun(&space, 1, aParams, aFlags); - nsAutoPtr textRun; textRun = new gfxTextRun(aParams, &space, 1, this, aFlags); - if (!textRun) + if (!textRun || !textRun->GetCharacterGlyphs()) return nsnull; - if (NS_FAILED(textRun->AddGlyphRun(font, 0))) - return nsnull; - gfxTextRun::CompressedGlyph g; - g.SetSimpleGlyph(spaceWidthAppUnits, spaceGlyph); - textRun->SetCharacterGlyph(0, g); + + gfxFont *font = GetFontAt(0); + textRun->SetSpaceGlyph(font, aParams->mContext, 0); return textRun.forget(); } @@ -691,35 +680,7 @@ gfxTextRun::Clone(const gfxTextRunFactory::Parameters *aParams, const void *aTex if (!textRun || !textRun->mCharacterGlyphs) return nsnull; - PRUint32 i; - for (i = 0; i < mGlyphRuns.Length(); ++i) { - if (NS_FAILED(textRun->AddGlyphRun(mGlyphRuns[i].mFont, - mGlyphRuns[i].mCharacterOffset))) - return nsnull; - } - - for (i = 0; i < aLength; ++i) { - CompressedGlyph g = mCharacterGlyphs[i]; - g.SetCanBreakBefore(PR_FALSE); - textRun->mCharacterGlyphs[i] = g; - } - - if (mDetailedGlyphs) { - for (i = 0; i < aLength; ++i) { - DetailedGlyph *details = mDetailedGlyphs[i]; - if (details) { - PRUint32 glyphCount = 1; - while (!details[glyphCount - 1].mIsLastGlyph) { - ++glyphCount; - } - DetailedGlyph *dest = textRun->AllocateDetailedGlyphs(i, glyphCount); - if (!dest) - return nsnull; - memcpy(dest, details, sizeof(DetailedGlyph)*glyphCount); - } - } - } - + textRun->CopyGlyphDataFrom(this, 0, mCharacterCount, 0, PR_FALSE); return textRun.forget(); } @@ -1490,8 +1451,24 @@ gfxTextRun::FindFirstGlyphRunContaining(PRUint32 aOffset) nsresult gfxTextRun::AddGlyphRun(gfxFont *aFont, PRUint32 aUTF16Offset) { - NS_ASSERTION(mGlyphRuns.Length() > 0 || aUTF16Offset == 0, + PRUint32 numGlyphRuns = mGlyphRuns.Length(); + if (numGlyphRuns > 0) { + GlyphRun *lastGlyphRun = &mGlyphRuns[numGlyphRuns - 1]; + + NS_ASSERTION(lastGlyphRun->mCharacterOffset <= aUTF16Offset, + "Glyph runs out of order"); + + if (lastGlyphRun->mFont == aFont) + return NS_OK; + if (lastGlyphRun->mCharacterOffset == aUTF16Offset) { + lastGlyphRun->mFont = aFont; + return NS_OK; + } + } + + NS_ASSERTION(numGlyphRuns > 0 || aUTF16Offset == 0, "First run doesn't cover the first character?"); + GlyphRun *glyphRun = mGlyphRuns.AppendElement(); if (!glyphRun) return NS_ERROR_OUT_OF_MEMORY; @@ -1584,3 +1561,122 @@ gfxTextRun::RecordSurrogates(const PRUnichar *aString) } } } + +static PRUint32 +CountDetailedGlyphs(gfxTextRun::DetailedGlyph *aGlyphs) +{ + PRUint32 i = 0; + while (!aGlyphs[i].mIsLastGlyph) { + ++i; + } + return i + 1; +} + +static void +ClearCharacters(gfxTextRun::CompressedGlyph *aGlyphs, PRUint32 aLength) +{ + gfxTextRun::CompressedGlyph g; + g.SetMissing(); + PRUint32 i; + for (i = 0; i < aLength; ++i) { + aGlyphs[i] = g; + } +} + +void +gfxTextRun::CopyGlyphDataFrom(gfxTextRun *aSource, PRUint32 aStart, + PRUint32 aLength, PRUint32 aDest, + PRBool aStealData) +{ + NS_ASSERTION(aStart + aLength <= aSource->GetLength(), + "Source substring out of range"); + NS_ASSERTION(aDest + aLength <= GetLength(), + "Destination substring out of range"); + + PRUint32 i; + for (i = 0; i < aLength; ++i) { + CompressedGlyph g = aSource->mCharacterGlyphs[i + aStart]; + g.SetCanBreakBefore(PR_FALSE); + mCharacterGlyphs[i + aDest] = g; + if (aStealData) { + aSource->mCharacterGlyphs[i + aStart].SetMissing(); + } + } + + if (aSource->mDetailedGlyphs) { + for (i = 0; i < aLength; ++i) { + DetailedGlyph *details = aSource->mDetailedGlyphs[i + aStart]; + if (details) { + if (aStealData) { + if (!mDetailedGlyphs) { + mDetailedGlyphs = new nsAutoArrayPtr[mCharacterCount]; + if (!mDetailedGlyphs) { + ClearCharacters(&mCharacterGlyphs[aDest], aLength); + return; + } + } + mDetailedGlyphs[i + aDest] = details; + aSource->mDetailedGlyphs[i + aStart].forget(); + } else { + PRUint32 glyphCount = CountDetailedGlyphs(details); + DetailedGlyph *dest = AllocateDetailedGlyphs(i + aDest, glyphCount); + if (!dest) { + ClearCharacters(&mCharacterGlyphs[aDest], aLength); + return; + } + memcpy(dest, details, sizeof(DetailedGlyph)*glyphCount); + } + } else if (mDetailedGlyphs) { + mDetailedGlyphs[i + aDest] = nsnull; + } + } + } else if (mDetailedGlyphs) { + for (i = 0; i < aLength; ++i) { + mDetailedGlyphs[i + aDest] = nsnull; + } + } + + GlyphRunIterator iter(aSource, aStart, aLength); + while (iter.NextRun()) { + gfxFont *font = iter.GetGlyphRun()->mFont; + PRUint32 start = iter.GetStringStart(); + PRUint32 end = iter.GetStringEnd(); + NS_ASSERTION(aSource->IsClusterStart(start), + "Started word in the middle of a cluster..."); + NS_ASSERTION(end == aSource->GetLength() || aSource->IsClusterStart(end), + "Ended word in the middle of a cluster..."); + + nsresult rv = AddGlyphRun(font, start - aStart + aDest); + if (NS_FAILED(rv)) + return; + } +} + +void +gfxTextRun::SetSpaceGlyph(gfxFont *aFont, gfxContext *aContext, PRUint32 aCharIndex) +{ + PRUint32 spaceGlyph = aFont->GetSpaceGlyph(); + float spaceWidth = aFont->GetMetrics().spaceWidth; + PRUint32 spaceWidthAppUnits = NS_lroundf(spaceWidth*mAppUnitsPerDevUnit); + if (!spaceGlyph || + !CompressedGlyph::IsSimpleGlyphID(spaceGlyph) || + !CompressedGlyph::IsSimpleAdvance(spaceWidthAppUnits)) { + gfxTextRunFactory::Parameters params = { + aContext, nsnull, nsnull, nsnull, 0, mAppUnitsPerDevUnit + }; + static const PRUint8 space = ' '; + nsAutoPtr textRun; + textRun = mFontGroup->MakeTextRun(&space, 1, ¶ms, + gfxTextRunFactory::TEXT_IS_8BIT | gfxTextRunFactory::TEXT_IS_ASCII | + gfxTextRunFactory::TEXT_IS_PERSISTENT); + if (!textRun || !textRun->mCharacterGlyphs) + return; + CopyGlyphDataFrom(textRun, 0, 1, aCharIndex, PR_TRUE); + return; + } + + AddGlyphRun(aFont, aCharIndex); + CompressedGlyph g; + g.SetSimpleGlyph(spaceWidthAppUnits, spaceGlyph); + SetCharacterGlyph(aCharIndex, g); +} diff --git a/gfx/thebes/src/gfxSkipChars.cpp b/gfx/thebes/src/gfxSkipChars.cpp index 0fd8dfb4dfbc..60bebf44bb7c 100644 --- a/gfx/thebes/src/gfxSkipChars.cpp +++ b/gfx/thebes/src/gfxSkipChars.cpp @@ -88,13 +88,10 @@ gfxSkipChars::BuildShortcuts() void gfxSkipCharsIterator::SetOffsets(PRUint32 aOffset, PRBool aInOriginalString) { + NS_ASSERTION(aOffset <= mSkipChars->mCharCount, + "Invalid offset"); + if (mSkipChars->mListLength == 0) { - // Special case: all chars kept, original and stripped strings are equal - if (aOffset < 0) { - aOffset = 0; - } else if (aOffset > mSkipChars->mCharCount) { - aOffset = mSkipChars->mCharCount; - } mOriginalStringOffset = mSkippedStringOffset = aOffset; return; } diff --git a/gfx/thebes/src/gfxTextRunWordCache.cpp b/gfx/thebes/src/gfxTextRunWordCache.cpp new file mode 100644 index 000000000000..b8f63473719e --- /dev/null +++ b/gfx/thebes/src/gfxTextRunWordCache.cpp @@ -0,0 +1,469 @@ +/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Foundation code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Vladimir Vukicevic + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include "gfxTextRunWordCache.h" + +static inline PRUint32 +HashMix(PRUint32 aHash, PRUnichar aCh) +{ + return (aHash >> 28) ^ (aHash << 4) ^ aCh; +} + +// If the substring of the textrun is rendered entirely in the first font +// of the textrun's fontgroup, then return that font. Otherwise return the +// fontgroup. +static void *GetWordFontOrGroup(gfxTextRun *aTextRun, PRUint32 aOffset, + PRUint32 aLength) +{ + PRUint32 glyphRunCount; + const gfxTextRun::GlyphRun *glyphRuns = aTextRun->GetGlyphRuns(&glyphRunCount); + PRUint32 glyphRunIndex = aTextRun->FindFirstGlyphRunContaining(aOffset); + gfxFontGroup *fontGroup = aTextRun->GetFontGroup(); + gfxFont *firstFont = fontGroup->GetFontAt(0); + if (glyphRuns[glyphRunIndex].mFont != firstFont) + return fontGroup; + + PRUint32 glyphRunEnd = glyphRunIndex == glyphRunCount - 1 + ? aTextRun->GetLength() : glyphRuns[glyphRunIndex + 1].mCharacterOffset; + if (aOffset + aLength <= glyphRunEnd) + return firstFont; + return fontGroup; +} + +#define UNICODE_NBSP 0x00A0 + +// XXX should we treat NBSP or SPACE combined with other characters as a word +// boundary? Currently this does. +static PRBool +IsBoundarySpace(PRUnichar aChar) +{ + return aChar == ' ' || aChar == UNICODE_NBSP; +} + +static PRBool +IsWordBoundary(PRUnichar aChar) +{ + return IsBoundarySpace(aChar) || gfxFontGroup::IsInvisibleChar(aChar); +} + +/** + * Looks up a word in the cache. If the word is found in the cache + * (which could be from an existing textrun or an earlier word in the same + * textrun), we copy the glyphs from the word into the textrun, unless + * aDeferredWords is non-null (meaning that all words from now on must be + * copied later instead of now), in which case we add the word to be copied + * to the list. + * + * If the word is not found in the cache then we add it to the cache with + * aFirstFont as the key, on the assumption that the word will be matched + * by aFirstFont. If this assumption is false we fix up the cache later in + * FinishTextRun. We make this assumption for two reasons: + * 1) it's usually true so it saves an extra cache lookup if we had to insert + * the entry later + * 2) we need to record words that appear in the string in some kind + * of hash table so we can detect and use them if they appear later in the + * (in general the string might be huge and contain many repeated words). + * We might as well use the master hash table for this. + * + * @return true if the word was found in the cache, false otherwise. + */ +PRBool +gfxTextRunWordCache::LookupWord(gfxTextRun *aTextRun, gfxFont *aFirstFont, + PRUint32 aStart, PRUint32 aEnd, PRUint32 aHash, + nsTArray* aDeferredWords) +{ + if (aEnd <= aStart) + return PR_TRUE; + + PRUint32 length = aEnd - aStart; + CacheHashKey key = + { aFirstFont, aTextRun->GetTextAt(aStart), + length, aTextRun->GetAppUnitsPerDevUnit(), aHash, + (aTextRun->GetFlags() & gfxTextRunFactory::TEXT_IS_8BIT) == 0 }; + CacheHashEntry *fontEntry = mCache.PutEntry(key); + if (!fontEntry) + return PR_FALSE; + CacheHashEntry *existingEntry = nsnull; + + if (fontEntry->mTextRun) { + existingEntry = fontEntry; + } else { + key.mFontOrGroup = aTextRun->GetFontGroup(); + CacheHashEntry *groupEntry = mCache.GetEntry(key); + if (groupEntry) { + existingEntry = groupEntry; + mCache.RawRemoveEntry(fontEntry); + fontEntry = nsnull; + } + } + // At this point, either existingEntry is non-null and points to (surprise!) + // an entry for a word in an existing textrun, or fontEntry points + // to a cache entry for this word with aFirstFont, which needs to be + // filled in or removed. + + if (existingEntry) { + if (aDeferredWords) { + DeferredWord word = { existingEntry->mTextRun, + existingEntry->mWordOffset, aStart, aEnd - aStart, aHash }; + aDeferredWords->AppendElement(word); + } else { + aTextRun->CopyGlyphDataFrom(existingEntry->mTextRun, + existingEntry->mWordOffset, aEnd - aStart, aStart, PR_FALSE); + } + return PR_TRUE; + } + + // Set up the cache entry so that if later in this textrun we hit this + // entry, we'll copy within our own textrun + fontEntry->mTextRun = aTextRun; + fontEntry->mWordOffset = aStart; + fontEntry->mHashedByFont = PR_TRUE; + return PR_FALSE; +} + +/** + * Processes all deferred words. Each word is copied from the source + * textrun to the output textrun. (The source may be an earlier word in the + * output textrun.) If the word was not matched by the textrun's fontgroup's + * first font, then we remove the entry we optimistically added to the cache + * with that font in the key, and add a new entry keyed with the fontgroup + * instead. + * + * @param aSuccessful if false, then we don't do any word copies and we don't + * add anything to the cache, but we still remove all the optimistic cache + * entries. + */ +void +gfxTextRunWordCache::FinishTextRun(gfxTextRun *aTextRun, gfxTextRun *aNewRun, + const nsTArray& aDeferredWords, + PRBool aSuccessful) +{ + PRUint32 i; + gfxFontGroup *fontGroup = aTextRun->GetFontGroup(); + gfxFont *font = fontGroup->GetFontAt(0); + // copy deferred words from various sources into destination textrun + for (i = 0; i < aDeferredWords.Length(); ++i) { + const DeferredWord *word = &aDeferredWords[i]; + gfxTextRun *source = word->mSourceTextRun; + if (!source) { + source = aNewRun; + // we created a cache entry for this word based on the assumption + // that the word matches GetFontAt(0). If this assumption is false, + // we need to remove that cache entry and replace it with an entry + // keyed off the fontgroup. + if (!aSuccessful || + GetWordFontOrGroup(aNewRun, word->mSourceOffset, word->mLength) != font) { + CacheHashKey key = + { font, aTextRun->GetTextAt(word->mDestOffset), + word->mLength, aTextRun->GetAppUnitsPerDevUnit(), word->mHash, + (aTextRun->GetFlags() & gfxTextRunFactory::TEXT_IS_8BIT) == 0 }; + NS_ASSERTION(mCache.GetEntry(key), + "This entry should have been added previously!"); + mCache.RemoveEntry(key); + + if (aSuccessful) { + key.mFontOrGroup = fontGroup; + CacheHashEntry *groupEntry = mCache.PutEntry(key); + if (groupEntry) { + groupEntry->mTextRun = aTextRun; + groupEntry->mWordOffset = word->mDestOffset; + groupEntry->mHashedByFont = PR_FALSE; + } + } + } + } + if (aSuccessful) { + // Copy the word. If the source is aNewRun, then + // allow CopyGlyphDataFrom to steal the internal data of + // aNewRun since that's only temporary anyway. + aTextRun->CopyGlyphDataFrom(source, + word->mSourceOffset, word->mLength, word->mDestOffset, + source == aNewRun); + } + } +} + +gfxTextRun * +gfxTextRunWordCache::MakeTextRun(const PRUnichar *aText, PRUint32 aLength, + gfxFontGroup *aFontGroup, + const gfxFontGroup::Parameters *aParams, + PRUint32 aFlags, PRBool *aIsInCache) +{ + nsAutoPtr textRun; + textRun = new gfxTextRun(aParams, aText, aLength, aFontGroup, aFlags); + if (!textRun || !textRun->GetCharacterGlyphs()) + return nsnull; + + gfxFont *font = aFontGroup->GetFontAt(0); + nsresult rv = textRun->AddGlyphRun(font, 0); + NS_ENSURE_SUCCESS(rv, nsnull); + + nsAutoTArray tempString; + nsAutoTArray deferredWords; + PRUint32 i; + PRUint32 wordStart = 0; + PRUint32 hash = 0; + for (i = 0; i <= aLength; ++i) { + PRUnichar ch = i < aLength ? aText[i] : ' '; + if (IsWordBoundary(ch)) { + PRBool hit = LookupWord(textRun, font, wordStart, i, hash, + deferredWords.Length() == 0 ? nsnull : &deferredWords); + if (!hit) { + if (tempString.Length() > 0) { + tempString.AppendElement(' '); + } + PRUint32 offset = tempString.Length(); + PRUint32 length = i - wordStart; + PRUnichar *chars = tempString.AppendElements(length); + if (!chars) { + FinishTextRun(textRun, nsnull, deferredWords, PR_FALSE); + return nsnull; + } + memcpy(chars, aText + wordStart, length*sizeof(PRUnichar)); + DeferredWord word = { nsnull, offset, wordStart, length, hash }; + deferredWords.AppendElement(word); + } + + if (IsBoundarySpace(ch) && i < aLength) { + textRun->SetSpaceGlyph(font, aParams->mContext, i); + } // else we should set this character to be invisible missing, + // but it already is because the textrun is blank! + hash = 0; + wordStart = i + 1; + } else { + hash = HashMix(hash, ch); + } + } + + if (deferredWords.Length() == 0) { + // We got everything from the cache, so we're done. No point in calling + // FinishTextRun. + // This textrun is not referenced by the cache. + *aIsInCache = PR_FALSE; + return textRun.forget(); + } + *aIsInCache = PR_TRUE; + + // create textrun for unknown words + gfxTextRunFactory::Parameters params = + { aParams->mContext, nsnull, nsnull, nsnull, 0, aParams->mAppUnitsPerDevUnit }; + nsAutoPtr newRun; + newRun = aFontGroup->MakeTextRun(tempString.Elements(), tempString.Length(), + ¶ms, aFlags | gfxTextRunFactory::TEXT_IS_PERSISTENT); + + FinishTextRun(textRun, newRun, deferredWords, newRun != nsnull); + return textRun.forget(); +} + +gfxTextRun * +gfxTextRunWordCache::MakeTextRun(const PRUint8 *aText, PRUint32 aLength, + gfxFontGroup *aFontGroup, + const gfxFontGroup::Parameters *aParams, + PRUint32 aFlags, PRBool *aIsInCache) +{ + aFlags |= gfxTextRunFactory::TEXT_IS_8BIT; + nsAutoPtr textRun; + textRun = new gfxTextRun(aParams, aText, aLength, aFontGroup, aFlags); + if (!textRun || !textRun->GetCharacterGlyphs()) + return nsnull; + + gfxFont *font = aFontGroup->GetFontAt(0); + nsresult rv = textRun->AddGlyphRun(font, 0); + NS_ENSURE_SUCCESS(rv, nsnull); + + nsAutoTArray tempString; + nsAutoTArray deferredWords; + PRUint32 i; + PRUint32 wordStart = 0; + PRUint32 hash = 0; + for (i = 0; i <= aLength; ++i) { + PRUint8 ch = i < aLength ? aText[i] : ' '; + if (IsWordBoundary(ch)) { + PRBool hit = LookupWord(textRun, font, wordStart, i, hash, + deferredWords.Length() == 0 ? nsnull : &deferredWords); + if (!hit) { + if (tempString.Length() > 0) { + tempString.AppendElement(' '); + } + PRUint32 offset = tempString.Length(); + PRUint32 length = i - wordStart; + PRUint8 *chars = tempString.AppendElements(length); + if (!chars) { + FinishTextRun(textRun, nsnull, deferredWords, PR_FALSE); + return nsnull; + } + memcpy(chars, aText + wordStart, length*sizeof(PRUint8)); + DeferredWord word = { nsnull, offset, wordStart, length, hash }; + deferredWords.AppendElement(word); + } + + if (IsBoundarySpace(ch) && i < aLength) { + textRun->SetSpaceGlyph(font, aParams->mContext, i); + } // else we should set this character to be invisible missing, + // but it already is because the textrun is blank! + hash = 0; + wordStart = i + 1; + } else { + hash = HashMix(hash, ch); + } + } + + if (deferredWords.Length() == 0) { + // We got everything from the cache, so we're done. No point in calling + // FinishTextRun. + // This textrun is not referenced by the cache. + *aIsInCache = PR_FALSE; + return textRun.forget(); + } + *aIsInCache = PR_TRUE; + + // create textrun for unknown words + gfxTextRunFactory::Parameters params = + { aParams->mContext, nsnull, nsnull, nsnull, 0, aParams->mAppUnitsPerDevUnit }; + nsAutoPtr newRun; + newRun = aFontGroup->MakeTextRun(tempString.Elements(), tempString.Length(), + ¶ms, aFlags | gfxTextRunFactory::TEXT_IS_PERSISTENT); + + FinishTextRun(textRun, newRun, deferredWords, newRun != nsnull); + return textRun.forget(); +} + +void +gfxTextRunWordCache::RemoveWord(gfxTextRun *aTextRun, PRUint32 aStart, + PRUint32 aEnd, PRUint32 aHash) +{ + if (aEnd <= aStart) + return; + + PRUint32 length = aEnd - aStart; + CacheHashKey key = + { GetWordFontOrGroup(aTextRun, aStart, length), aTextRun->GetTextAt(aStart), + length, aTextRun->GetAppUnitsPerDevUnit(), aHash, + (aTextRun->GetFlags() & gfxTextRunFactory::TEXT_IS_8BIT) == 0 }; + CacheHashEntry *entry = mCache.GetEntry(key); + if (entry && entry->mTextRun == aTextRun) { + // XXX would like to use RawRemoveEntry here plus some extra method + // that conditionally shrinks the hashtable + mCache.RemoveEntry(key); + } +} + +// Remove a textrun from the cache by looking up each word and removing it +void +gfxTextRunWordCache::RemoveTextRun(gfxTextRun *aTextRun) +{ + PRUint32 i; + PRUint32 wordStart = 0; + PRUint32 hash = 0; + for (i = 0; i < aTextRun->GetLength(); ++i) { + PRUnichar ch = aTextRun->GetChar(i); + if (IsWordBoundary(ch)) { + RemoveWord(aTextRun, wordStart, i, hash); + hash = 0; + wordStart = i + 1; + } else { + hash = HashMix(hash, ch); + } + } + RemoveWord(aTextRun, wordStart, i, hash); +} + +static PRBool +CompareDifferentWidthStrings(const PRUint8 *aStr1, const PRUnichar *aStr2, + PRUint32 aLength) +{ + PRUint32 i; + for (i = 0; i < aLength; ++i) { + if (aStr1[i] != aStr2[i]) + return PR_FALSE; + } + return PR_TRUE; +} + +static PRBool +IsWordEnd(gfxTextRun *aTextRun, PRUint32 aOffset) +{ + PRUint32 runLength = aTextRun->GetLength(); + if (aOffset == runLength) + return PR_TRUE; + if (aOffset > runLength) + return PR_FALSE; + return IsWordBoundary(aTextRun->GetChar(aOffset)); +} + +static void * +GetFontOrGroup(gfxFontGroup *aFontGroup, PRBool aUseFont) +{ + return aUseFont + ? NS_STATIC_CAST(void *, aFontGroup->GetFontAt(0)) + : NS_STATIC_CAST(void *, aFontGroup); +} + +PRBool +gfxTextRunWordCache::CacheHashEntry::KeyEquals(const KeyTypePointer aKey) const +{ + if (!mTextRun) + return PR_FALSE; + + PRUint32 length = aKey->mLength; + gfxFontGroup *fontGroup = mTextRun->GetFontGroup(); + if (!IsWordEnd(mTextRun, mWordOffset + length) || + GetFontOrGroup(fontGroup, mHashedByFont) != aKey->mFontOrGroup || + aKey->mAppUnitsPerDevUnit != mTextRun->GetAppUnitsPerDevUnit()) + return PR_FALSE; + + if (mTextRun->GetFlags() & gfxFontGroup::TEXT_IS_8BIT) { + const PRUint8 *text = mTextRun->GetText8Bit() + mWordOffset; + if (!aKey->mIsDoubleByteText) + return memcmp(text, aKey->mString, length) == 0; + return CompareDifferentWidthStrings(text, + NS_STATIC_CAST(const PRUnichar *, aKey->mString), length); + } else { + const PRUnichar *text = mTextRun->GetTextUnicode() + mWordOffset; + if (aKey->mIsDoubleByteText) + return memcmp(text, aKey->mString, length*sizeof(PRUnichar)) == 0; + return CompareDifferentWidthStrings(NS_STATIC_CAST(const PRUint8 *, aKey->mString), + text, length); + } +} + +PLDHashNumber +gfxTextRunWordCache::CacheHashEntry::HashKey(const KeyTypePointer aKey) +{ + return aKey->mStringHash + (long)aKey->mFontOrGroup + aKey->mAppUnitsPerDevUnit + + aKey->mIsDoubleByteText; +}