diff --git a/Src/Common/RootSite/RootSiteTests/RealDataTestsBase.cs b/Src/Common/RootSite/RootSiteTests/RealDataTestsBase.cs
index f5717a0f42..5cc79d112a 100644
--- a/Src/Common/RootSite/RootSiteTests/RealDataTestsBase.cs
+++ b/Src/Common/RootSite/RootSiteTests/RealDataTestsBase.cs
@@ -28,6 +28,7 @@ public abstract class RealDataTestsBase
private static readonly string ProjectMutexName =
@"Local\FieldWorks.RealDataTests." + ReusableProjectName;
private const string TestProjectSentinelFileName = ".fieldworks-real-data-test-project";
+ private const string ProjectDataFileExtension = ".fwdata";
private static string WorktreeSuffix()
{
@@ -298,6 +299,28 @@ private static string GetProjectDirectory(string createdPath)
return projectDirectory;
}
+ ///
+ /// Reports whether a directory holds no FieldWorks project data other than the file this
+ /// fixture generates for its own reusable project name.
+ ///
+ private static bool HoldsOnlyGeneratedProjectData(string projectDirectory)
+ {
+ var generatedDataFileName = ReusableProjectName + ProjectDataFileExtension;
+ foreach (var dataFile in Directory.GetFiles(
+ projectDirectory, "*" + ProjectDataFileExtension))
+ {
+ if (!string.Equals(
+ Path.GetFileName(dataFile),
+ generatedDataFileName,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
private static void WriteTestProjectSentinel(string projectDirectory)
{
EnsureSafeProjectDirectory(projectDirectory);
@@ -317,13 +340,28 @@ private static void DeleteProjectDirectory(string projectDirectory)
if (!File.Exists(GetSentinelFilePath(safeProjectDirectory)))
{
- throw new InvalidOperationException(
- string.Format(
- "Refusing to delete '{0}' because the test sentinel file '{1}' is missing.",
- safeProjectDirectory,
- TestProjectSentinelFileName
- )
+ // Project creation makes this directory before the sentinel can be written, so a run
+ // interrupted during CreateNewLangProj leaves the directory without one. The directory
+ // still identifies itself as generated test data: EnsureSafeProjectDirectory has
+ // matched its worktree-hashed name, and it holds no project data beyond this
+ // fixture's own. Restoring the sentinel keeps that state from blocking every later run.
+ if (!HoldsOnlyGeneratedProjectData(safeProjectDirectory))
+ {
+ throw new InvalidOperationException(
+ string.Format(
+ "Refusing to delete '{0}' because the test sentinel file '{1}' is missing " +
+ "and it holds project data this fixture did not generate.",
+ safeProjectDirectory,
+ TestProjectSentinelFileName
+ )
+ );
+ }
+
+ TestContext.Progress.WriteLine(
+ "Restoring the missing test sentinel in '{0}' before deleting it.",
+ safeProjectDirectory
);
+ WriteTestProjectSentinel(safeProjectDirectory);
}
if (!RobustIO.DeleteDirectoryAndContents(safeProjectDirectory))
diff --git a/Src/Common/RootSite/RootSiteTests/RenderBenchmarkTestsBase.cs b/Src/Common/RootSite/RootSiteTests/RenderBenchmarkTestsBase.cs
index 679a532456..e84bdbc123 100644
--- a/Src/Common/RootSite/RootSiteTests/RenderBenchmarkTestsBase.cs
+++ b/Src/Common/RootSite/RootSiteTests/RenderBenchmarkTestsBase.cs
@@ -25,6 +25,8 @@ namespace SIL.FieldWorks.Common.RootSites.RenderBenchmark
public abstract class RenderBenchmarkTestsBase : RealDataTestsBase
{
protected const string DeterministicRenderFontFamily = "Segoe UI";
+ // Second Latin font for writing-system-alternation scenarios.
+ protected const string SecondaryRenderFontFamily = "Times New Roman";
// Pinned Arabic font (loaded privately by RenderTestAssemblySetup). Used for Arabic runs so
// they don't depend on the host's Segoe UI Arabic version / font fallback.
protected const string ArabicRenderFontFamily = "Scheherazade New";
@@ -320,6 +322,15 @@ protected void SetupScenarioData(string scenarioId)
case "multi-ws":
CreateMultiWsScenario();
break;
+ case "single-para-mixed-ws":
+ CreateSingleParaMixedWsScenario();
+ break;
+ case "nfc-composable-diacritics":
+ CreateNfcComposableDiacriticsScenario();
+ break;
+ case "multi-line-wrap-single-ws":
+ CreateMultiLineWrapSingleWsScenario();
+ break;
case "lex-shallow":
CreateLexEntryScenario(depth: 2, breadth: 3);
break;
@@ -538,6 +549,30 @@ private void CreateMultiWsScenario()
AddMultiWsSections(book, 5, versesPerSection: 8, chapterStart: 1);
}
+ /// Single paragraph of many short sentences alternating two writing systems.
+ private void CreateSingleParaMixedWsScenario()
+ {
+ var book = CreateBook(19); // PSA
+ m_hvoRoot = book.Hvo;
+ AddSingleMixedWsParagraph(book, sentenceCount: 236);
+ }
+
+ /// Single wrapped paragraph of Latin words spelled with decomposed diacritics.
+ private void CreateNfcComposableDiacriticsScenario()
+ {
+ var book = CreateBook(15); // EZR
+ m_hvoRoot = book.Hvo;
+ AddNfcComposableDiacriticsParagraph(book, wordCount: 80);
+ }
+
+ /// Single long wrapped paragraph of unique sentences in one writing system.
+ private void CreateMultiLineWrapSingleWsScenario()
+ {
+ var book = CreateBook(17); // EST
+ m_hvoRoot = book.Hvo;
+ AddSingleWsProseParagraph(book, sentenceCount: 200);
+ }
+
#region Rich Data Factories
protected IScrBook CreateBook(int bookNum)
@@ -927,6 +962,132 @@ protected void AddMultiWsSections(IScrBook book, int sectionCount,
}
}
+ /// Single paragraph of alternating-writing-system sentences with decomposed diacritics.
+ protected void AddSingleMixedWsParagraph(IScrBook book, int sentenceCount)
+ {
+ var section = Cache.ServiceLocator.GetInstance().Create();
+ book.SectionsOS.Add(section);
+
+ var stTextFactory = Cache.ServiceLocator.GetInstance();
+
+ section.HeadingOA = stTextFactory.Create();
+ var headingBldr = new StTxtParaBldr(Cache) { ParaStyleName = ScrStyleNames.SectionHead };
+ headingBldr.AppendRun("Single Paragraph, Mixed Writing Systems",
+ StyleUtils.CharStyleTextProps(null, m_wsEng));
+ headingBldr.CreateParagraph(section.HeadingOA);
+
+ string[] subjects =
+ {
+ "the élder", "the hèrder", "the sïnger",
+ "the teaçher", "the travêler", "the womãn"
+ };
+ string[] predicates =
+ {
+ "spoke of the lóng rains", "walked to the fàr well", "named the sevën hills",
+ "counted the cattlé at dusk", "kept the ôld story", "asked for a blessĩng"
+ };
+
+ section.ContentOA = stTextFactory.Create();
+ var paraBldr = new StTxtParaBldr(Cache) { ParaStyleName = ScrStyleNames.NormalParagraph };
+ paraBldr.AppendRun("1", StyleUtils.CharStyleTextProps(ScrStyleNames.ChapterNumber, m_wsEng));
+
+ for (int i = 0; i < sentenceCount; i++)
+ {
+ string sentence = $"{subjects[i % subjects.Length]} {predicates[i % predicates.Length]} {i + 1}. ";
+ paraBldr.AppendRun(sentence, AlternatingFontRunProps(i % 2 == 0));
+ }
+
+ paraBldr.CreateParagraph(section.ContentOA);
+ }
+
+ /// Run properties alternating writing system and font family together.
+ private ITsTextProps AlternatingFontRunProps(bool first)
+ {
+ return AlternatingFontRunProps(first, first);
+ }
+
+ /// Run properties combining an independently chosen writing system and font family.
+ private ITsTextProps AlternatingFontRunProps(bool useFirstWs, bool useFirstFont)
+ {
+ var bldr = TsStringUtils.MakePropsBldr();
+ bldr.SetIntPropValues((int)FwTextPropType.ktptWs, (int)FwTextPropVar.ktpvDefault,
+ useFirstWs ? m_wsEng : m_wsFr);
+ bldr.SetStrPropValue((int)FwTextPropType.ktptFontFamily,
+ useFirstFont ? DeterministicRenderFontFamily : SecondaryRenderFontFamily);
+ return bldr.GetTextProps();
+ }
+
+ /// Single wrapped paragraph of Latin words spelled with decomposed diacritics.
+ protected void AddNfcComposableDiacriticsParagraph(IScrBook book, int wordCount)
+ {
+ var section = Cache.ServiceLocator.GetInstance().Create();
+ book.SectionsOS.Add(section);
+
+ var stTextFactory = Cache.ServiceLocator.GetInstance();
+
+ section.HeadingOA = stTextFactory.Create();
+ var headingBldr = new StTxtParaBldr(Cache) { ParaStyleName = ScrStyleNames.SectionHead };
+ headingBldr.AppendRun("Decomposed Diacritics Microbenchmark",
+ StyleUtils.CharStyleTextProps(null, m_wsEng));
+ headingBldr.CreateParagraph(section.HeadingOA);
+
+ string[] decomposedWords =
+ {
+ "café", "déjà", "noël", "français", "garçon",
+ "hôtel", "año", "créée", "élégant", "façade",
+ };
+
+ section.ContentOA = stTextFactory.Create();
+ var paraBldr = new StTxtParaBldr(Cache) { ParaStyleName = ScrStyleNames.NormalParagraph };
+
+ for (int i = 0; i < wordCount; i++)
+ {
+ string word = decomposedWords[i % decomposedWords.Length];
+ paraBldr.AppendRun($"The {word} recorded here is entry {i + 1}. ",
+ StyleUtils.CharStyleTextProps(null, m_wsEng));
+ }
+
+ paraBldr.CreateParagraph(section.ContentOA);
+ }
+
+ /// Single long wrapped paragraph of unique sentences with decomposed diacritics, one writing system.
+ protected void AddSingleWsProseParagraph(IScrBook book, int sentenceCount)
+ {
+ var section = Cache.ServiceLocator.GetInstance().Create();
+ book.SectionsOS.Add(section);
+
+ var stTextFactory = Cache.ServiceLocator.GetInstance();
+
+ section.HeadingOA = stTextFactory.Create();
+ var headingBldr = new StTxtParaBldr(Cache) { ParaStyleName = ScrStyleNames.SectionHead };
+ headingBldr.AppendRun("Single Writing-System Line-Wrap Microbenchmark",
+ StyleUtils.CharStyleTextProps(null, m_wsEng));
+ headingBldr.CreateParagraph(section.HeadingOA);
+
+ string[] subjects =
+ {
+ "the merçhant", "the masón", "the scribë",
+ "the shephèrd", "the weavêr", "the pottér"
+ };
+ string[] predicates =
+ {
+ "measured the grain by the rivér", "repaired the eastèrn wall before dusk",
+ "copied the ledger onto fresh parchmënt", "counted the flock past the old gatê",
+ "dyed the cloth a deep saffrõn", "shaped the jar on the slow wheël"
+ };
+
+ section.ContentOA = stTextFactory.Create();
+ var paraBldr = new StTxtParaBldr(Cache) { ParaStyleName = ScrStyleNames.NormalParagraph };
+
+ for (int i = 0; i < sentenceCount; i++)
+ {
+ string sentence = $"{subjects[i % subjects.Length]} {predicates[i % predicates.Length]} on day {i + 1}. ";
+ paraBldr.AppendRun(sentence, StyleUtils.CharStyleTextProps(null, m_wsEng));
+ }
+
+ paraBldr.CreateParagraph(section.ContentOA);
+ }
+
#endregion
#region Lex Entry Scenario Data
diff --git a/Src/Common/RootSite/RootSiteTests/RenderVerifyTests.VerifyScenario_multi-line-wrap-single-ws.verified.png b/Src/Common/RootSite/RootSiteTests/RenderVerifyTests.VerifyScenario_multi-line-wrap-single-ws.verified.png
new file mode 100644
index 0000000000..e32f9782af
Binary files /dev/null and b/Src/Common/RootSite/RootSiteTests/RenderVerifyTests.VerifyScenario_multi-line-wrap-single-ws.verified.png differ
diff --git a/Src/Common/RootSite/RootSiteTests/RenderVerifyTests.VerifyScenario_nfc-composable-diacritics.verified.png b/Src/Common/RootSite/RootSiteTests/RenderVerifyTests.VerifyScenario_nfc-composable-diacritics.verified.png
new file mode 100644
index 0000000000..b918ba4335
Binary files /dev/null and b/Src/Common/RootSite/RootSiteTests/RenderVerifyTests.VerifyScenario_nfc-composable-diacritics.verified.png differ
diff --git a/Src/Common/RootSite/RootSiteTests/RenderVerifyTests.VerifyScenario_single-para-mixed-ws.verified.png b/Src/Common/RootSite/RootSiteTests/RenderVerifyTests.VerifyScenario_single-para-mixed-ws.verified.png
new file mode 100644
index 0000000000..2336b0732a
Binary files /dev/null and b/Src/Common/RootSite/RootSiteTests/RenderVerifyTests.VerifyScenario_single-para-mixed-ws.verified.png differ
diff --git a/Src/Common/RootSite/RootSiteTests/TestData/RenderBenchmarkScenarios.json b/Src/Common/RootSite/RootSiteTests/TestData/RenderBenchmarkScenarios.json
index 782b92e682..4ec11f73e1 100644
--- a/Src/Common/RootSite/RootSiteTests/TestData/RenderBenchmarkScenarios.json
+++ b/Src/Common/RootSite/RootSiteTests/TestData/RenderBenchmarkScenarios.json
@@ -89,6 +89,21 @@
"description": "Lex entry with senses nested 6 levels deep, 2-wide (depth 6, breadth 2 = 126 senses)",
"tags": ["lex-entry", "nested-senses", "exponential-cost", "stress"],
"viewType": "LexEntry"
+ },
+ {
+ "id": "single-para-mixed-ws",
+ "description": "One paragraph, 236 unique short sentences alternating two writing systems, spelled with decomposed diacritics (base letter plus combining mark)",
+ "tags": ["stress", "layout-stress", "multi-ws", "single-paragraph", "shape-cache", "nfc"]
+ },
+ {
+ "id": "nfc-composable-diacritics",
+ "description": "Single paragraph of Latin prose spelled with decomposed diacritics (base letter plus combining mark), wrapping over several lines — exercises NFC index-map translation during line-break retries",
+ "tags": ["stress", "layout-stress", "nfc", "diacritics", "line-breaking"]
+ },
+ {
+ "id": "multi-line-wrap-single-ws",
+ "description": "Single long paragraph of unique English sentences spelled with decomposed diacritics (base letter plus combining mark), one writing system and font family throughout, wrapping over many lines",
+ "tags": ["stress", "layout-stress", "single-paragraph", "line-breaking", "nfc"]
}
]
}
diff --git a/Src/views/Test/testViews.mak b/Src/views/Test/testViews.mak
index 1b2c2adf54..4835c5ea4b 100644
--- a/Src/views/Test/testViews.mak
+++ b/Src/views/Test/testViews.mak
@@ -154,6 +154,7 @@ $(VIEWSTEST_SRC)\Collection.cpp: $(VIEWSTEST_SRC)\DummyBaseVc.h $(VIEWSTEST_SRC)
$(VIEWSTEST_SRC)\TestTsStrBldr.h\
$(VIEWSTEST_SRC)\TestTsString.h\
$(VIEWSTEST_SRC)\TestTsPropsBldr.h\
- $(VIEWSTEST_SRC)\TestTsTextProps.h
+ $(VIEWSTEST_SRC)\TestTsTextProps.h\
+ $(VIEWSTEST_SRC)\TestViewCaches.h
$(DISPLAY) Collecting tests for $(BUILD_PRODUCT).$(BUILD_EXTENSION)
$(COLLECT) $** $(VIEWSTEST_SRC)\Collection.cpp
diff --git a/Src/views/lib/UniscribeSegment.cpp b/Src/views/lib/UniscribeSegment.cpp
index f40bd24bfc..f034b34cfd 100644
--- a/Src/views/lib/UniscribeSegment.cpp
+++ b/Src/views/lib/UniscribeSegment.cpp
@@ -29,8 +29,6 @@ DEFINE_THIS_FILE
//:>********************************************************************************************
//:> Forward declarations
//:>********************************************************************************************
-static void BuildNfcOffsetMaps(const StrUni & stuOrig, Vector & vichOrigToNfc,
- Vector & vichNfcToOrig);
static void ApplyShapeRunCacheEntry(ShapeRunEntry & entry, UniscribeRunInfo & uri);
//:>********************************************************************************************
@@ -1702,30 +1700,6 @@ int UniscribeSegment::OffsetToOrig(int ich, int ichBase, IVwTextSource * pts, bo
return OffsetToOrig(ich, ichBase, pts, fTextIsNfc);
}
-static void BuildNfcOffsetMaps(const StrUni & stuOrig, Vector & vichOrigToNfc,
- Vector & vichNfcToOrig)
-{
- int cchOrig = stuOrig.Length();
- vichOrigToNfc.Resize(cchOrig + 1);
- vichOrigToNfc[0] = 0;
- for (int ich = 1; ich <= cchOrig; ++ich)
- {
- StrUni stuPrefix(stuOrig.Chars(), ich);
- StrUtil::NormalizeStrUni(stuPrefix, UNORM_NFC);
- vichOrigToNfc[ich] = stuPrefix.Length();
- }
-
- int cchNfc = vichOrigToNfc[cchOrig];
- vichNfcToOrig.Resize(cchNfc + 1);
- int ichOrig = 0;
- for (int ichNfc = 0; ichNfc <= cchNfc; ++ichNfc)
- {
- while (ichOrig + 1 <= cchOrig && vichOrigToNfc[ichOrig + 1] <= ichNfc)
- ++ichOrig;
- vichNfcToOrig[ichNfc] = ichOrig;
- }
-}
-
static void ApplyShapeRunCacheEntry(ShapeRunEntry & entry, UniscribeRunInfo & uri)
{
if (uri.CGlyphMax() < entry.m_cglyph)
@@ -3135,6 +3109,8 @@ int UniscribeSegment::CallScriptItemize(OLECHAR * prgchDefBuf, int cchBuf,
Vector vichOrigToNfc;
Vector vichNfcToOrig;
+ // Only text that normalization leaves unchanged is eligible for the analysis cache.
+ bool fTextEligibleForCache = true;
#ifdef UNISCRIBE_NFC
if (cch)
@@ -3154,8 +3130,7 @@ int UniscribeSegment::CallScriptItemize(OLECHAR * prgchDefBuf, int cchBuf,
bool fComputedTextIsNfc = (stu == stuOrig);
if (pfTextIsNfc)
*pfTextIsNfc = fComputedTextIsNfc;
- if (!fComputedTextIsNfc && pLayoutPassCache)
- BuildNfcOffsetMaps(stuOrig, vichOrigToNfc, vichNfcToOrig);
+ fTextEligibleForCache = fComputedTextIsNfc;
if (cch > cchBuf)
{
cchBuf = cch;
@@ -3317,10 +3292,14 @@ typedef struct tag_SCRIPT_STATE {
if (pLayoutPassCache)
{
pLayoutPassCache->AnalysisCache().AddComputeMs(::GetTickCount() - dwStartMs);
- TextAnalysisEntry * pStoredAnalysis = pLayoutPassCache->AnalysisCache().Store(pts, ichMin,
- cchOrig, ws, fWsRtl, *pprgchBuf, cch, pfTextIsNfc ? *pfTextIsNfc : true,
- g_vscri.Begin(), citem, vichOrigToNfc.Size() ? &vichOrigToNfc : NULL,
- vichNfcToOrig.Size() ? &vichNfcToOrig : NULL);
+ TextAnalysisEntry * pStoredAnalysis = NULL;
+ if (fTextEligibleForCache)
+ {
+ pStoredAnalysis = pLayoutPassCache->AnalysisCache().Store(pts, ichMin,
+ cchOrig, ws, fWsRtl, *pprgchBuf, cch, true,
+ g_vscri.Begin(), citem, vichOrigToNfc.Size() ? &vichOrigToNfc : NULL,
+ vichNfcToOrig.Size() ? &vichNfcToOrig : NULL);
+ }
if (ppAnalysis)
*ppAnalysis = pStoredAnalysis;
}