From 6e4f1830f20e9d4f5f2abec7d2e85180e5ea6643 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 13 Oct 2026 07:21:49 -0400 Subject: [PATCH 1/4] llrender's tests are Catch2 binaries, converted by tut2catch.py TM22, the fifth wave of stage 3: llrender's 26 TUT files, 392 tests, become three Catch2 binaries, each file still run in a process of its own, cut where the GL context's lifetime differs: - llrender_tests, 7 files that need no GL: the UI transform, the matrix stack and projection helpers, and the font tables and shaping. It links ICU for alfontshaping, which holds HarfBuzz's tables against it; - llrender_gl_fresh_tests, 6 files whose cases each make a context and end the GL classes with it: the rasterizing paths of the font libraries, and llimagegl, which also makes its context again in the middle of a case; - llrender_gl_shared_tests, 13 files whose cases share one context for the process through sharedHeadlessGL() and SharedGLScope. A fresh case ends the classes a shared context needs, so the two kinds never share a binary. The files are tut2catch.py's output, with the names the idiom pass that follows chose. It marked nothing TM-CHECK, and nothing was edited by hand. tut_parity.py, against the TUT executables' recorded results (392 tests: 362 ok and 30 skipped here, on a GL 4.1 context with no compute and no clip control), holds plain for all 26 files, with seven assertion sites reviewed as never running in a passing case on this host: - alfontshaping's two-glyph branch and its fail-only else, where this font composes the combining pair into one glyph; - algpubuffer's clear of a static buffer, only where it clears on the GPU; - three of llglslshader's checks, only where the context runs compute. All 32 entries pass, and so do the three combined runs. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/CMakeLists.txt | 133 +- indra/llrender/tests/alfontcolrv1_gl_test.cpp | 177 +- indra/llrender/tests/alfontcolrv1_test.cpp | 418 ++-- .../llrender/tests/alfontshaping_gl_test.cpp | 90 +- indra/llrender/tests/alfontshaping_test.cpp | 973 ++++---- indra/llrender/tests/algpubuffer_gl_test.cpp | 286 ++- indra/llrender/tests/alimageslot_gl_test.cpp | 281 ++- .../tests/alinfiniteprojection_gl_test.cpp | 191 +- .../tests/alinstanceddraw_gl_test.cpp | 46 +- .../llrender/tests/alrenderstate_gl_test.cpp | 159 +- .../llrender/tests/altessellation_gl_test.cpp | 113 +- .../tests/alwindowsdlheadless_test.cpp | 60 +- .../tests/llfontbitmapcache_gl_test.cpp | 347 +-- .../llrender/tests/llfontbitmapcache_test.cpp | 87 +- .../llrender/tests/llfontfreetype_gl_test.cpp | 248 +- indra/llrender/tests/llfontfreetype_test.cpp | 937 ++++---- indra/llrender/tests/llfontgl_test.cpp | 1056 +++++---- .../llrender/tests/llfontregistry_gl_test.cpp | 308 +-- indra/llrender/tests/llfontregistry_test.cpp | 2034 +++++++++-------- indra/llrender/tests/llfonttextcache_test.cpp | 359 +-- indra/llrender/tests/llglslshader_gl_test.cpp | 857 ++++--- indra/llrender/tests/llimagegl_test.cpp | 642 +++--- .../tests/llrender2dutils_gl_test.cpp | 44 +- indra/llrender/tests/llrendermatrix_test.cpp | 58 +- .../llrender/tests/llrendertarget_gl_test.cpp | 121 +- indra/llrender/tests/lluiimage_test.cpp | 58 +- indra/llrender/tests/lluitransform_test.cpp | 98 +- 27 files changed, 5638 insertions(+), 4543 deletions(-) diff --git a/indra/llrender/CMakeLists.txt b/indra/llrender/CMakeLists.txt index e7eb2965ef..aab0d636d2 100644 --- a/indra/llrender/CMakeLists.txt +++ b/indra/llrender/CMakeLists.txt @@ -133,64 +133,87 @@ if(AL_BUILD_TESTS) "LLFONT_TEST_FONTS_XML=\"${font_test_fonts_xml}\"" ) - # Arithmetic only: where a local coordinate lands under the UI transform. - al_add_test(lluitransform PROJECT llrender LIBRARIES llrender) - - # Arithmetic only: the matrix stack, the projection helpers and the - # clip-plane classes against the scalar matrix and the formulas. - al_add_test(llrendermatrix PROJECT llrender LIBRARIES llrender) - - # The font suites: tables and shaping, no GL. - foreach(test alfontcolrv1 llfontregistry llfontbitmapcache llfontfreetype) - al_add_test(${test} PROJECT llrender LIBRARIES llrender DEFINES ${font_defines}) - endforeach() - # Both Unicode tables in one place, so the test can hold them against each - # other. llrender itself has no need of ICU. - al_add_test(alfontshaping PROJECT llrender LIBRARIES llrender ll::icu DEFINES ${font_defines}) - - # The GL-backed suites, on LLWindowManager's hidden window: the paths of - # the four font libraries above that rasterize, and the wholly-GL ones. - foreach( - test - alfontcolrv1_gl - llfontregistry_gl - llfontbitmapcache_gl - llfontfreetype_gl - llfontgl - llfonttextcache - llimagegl + # The suites that need no GL: arithmetic (where a local coordinate lands + # under the UI transform; the matrix stack, the projection helpers and the + # clip-plane classes), and the font tables and shaping. alfontshaping holds + # both Unicode tables against each other, so this binary links ICU, which + # llrender itself has no need of. + al_add_test_binary( + llrender_tests + PROJECT llrender + SOURCES + tests/alfontcolrv1_test.cpp + tests/alfontshaping_test.cpp + tests/llfontbitmapcache_test.cpp + tests/llfontfreetype_test.cpp + tests/llfontregistry_test.cpp + tests/llrendermatrix_test.cpp + tests/lluitransform_test.cpp + LIBRARIES llrender ll::icu + DEFINES ${font_defines} ) - al_add_test(${test} GL PROJECT llrender LIBRARIES llrender DEFINES ${font_defines}) - endforeach() - al_add_test( - alfontshaping_gl - GL + + # The GL suites that stand a context up for each test case and end the GL + # classes with it, on LLWindowManager's hidden window: the paths of the + # font libraries above that rasterize, and llimagegl, which also makes a + # context again in the middle of a case. Never beside the shared context + # below, whose classes they would end under it. + al_add_test_binary( + llrender_gl_fresh_tests PROJECT llrender + SOURCES + tests/alfontcolrv1_gl_test.cpp + tests/alfontshaping_gl_test.cpp + tests/llfontbitmapcache_gl_test.cpp + tests/llfontfreetype_gl_test.cpp + tests/llfontregistry_gl_test.cpp + tests/llimagegl_test.cpp LIBRARIES llrender ll::icu DEFINES ${font_defines} + GL + ) + + # The GL suites that share one context for the process, each case putting + # back what it changed of it: + # - fonts drawn, cached and laid out (llfontgl, llfonttextcache), UI images + # and the immediate-mode 2D helpers' quads; + # - the hidden window itself, and a worker thread's shared context on it; + # - tessellation stages and the quad conventions the terrain relies on; + # - the infinite reverse-Z projection: stored depth, round trip and depth + # order out to 100 km; + # - which fragment output reaches a render target's colour attachment, and + # integer attachments cleared and drawn; + # - LLRender's cached clear colour and scissor box, and LLGLSScissor; + # - LLGLSLShader's uniform value cache, the compute path (programs built + # through the shader manager, their images, storage blocks and + # dispatches), integer textures through reserved samplers, what the debug + # validator says of images and stencil reads, and which shader objects a + # program owns and which the manager does; + # - LLRender::drawArraysInstanced: attribute-less instanced geometry, placed + # by the matrices the draw syncs; + # - ALGPUBuffer's store, LLRender's indirect buffer bindings over its + # lifetime, and the readback and stream rings; + # - ALImageSlot: what a unit issues, re-issues and forgets as LLRender keeps + # it, and integer images. + al_add_test_binary( + llrender_gl_shared_tests + PROJECT llrender + SOURCES + tests/algpubuffer_gl_test.cpp + tests/alimageslot_gl_test.cpp + tests/alinfiniteprojection_gl_test.cpp + tests/alinstanceddraw_gl_test.cpp + tests/alrenderstate_gl_test.cpp + tests/altessellation_gl_test.cpp + tests/alwindowsdlheadless_test.cpp + tests/llfontgl_test.cpp + tests/llfonttextcache_test.cpp + tests/llglslshader_gl_test.cpp + tests/llrender2dutils_gl_test.cpp + tests/llrendertarget_gl_test.cpp + tests/lluiimage_test.cpp + LIBRARIES llrender + DEFINES ${font_defines} + GL ) - al_add_test(lluiimage GL PROJECT llrender LIBRARIES llrender) - # The quads the immediate-mode 2D helpers emit. - al_add_test(llrender2dutils_gl GL PROJECT llrender LIBRARIES llrender) - # The hidden window itself, and a worker thread's shared context on it. - al_add_test(alwindowsdlheadless GL PROJECT llrender LIBRARIES llrender) - # Tessellation stages and the quad conventions the terrain relies on. - al_add_test(altessellation_gl GL PROJECT llrender LIBRARIES llrender) - # Infinite reverse-Z projection: stored depth, round trip and depth order out to 100 km. - al_add_test(alinfiniteprojection_gl GL PROJECT llrender LIBRARIES llrender) - # Which fragment output reaches a render target's colour attachment, and integer attachments cleared and drawn. - al_add_test(llrendertarget_gl GL PROJECT llrender LIBRARIES llrender) - # LLRender's cached clear colour and scissor box, and LLGLSScissor. - al_add_test(alrenderstate_gl GL PROJECT llrender LIBRARIES llrender) - # LLGLSLShader's uniform value cache: what a program holds after each kind of set, and across a relink; the - # compute path: programs built through the shader manager, their images, storage blocks and dispatches; integer - # textures through reserved samplers; what the debug validator says of images and stencil reads; and which shader - # objects a program owns and which the manager does. - al_add_test(llglslshader_gl GL PROJECT llrender LIBRARIES llrender) - # LLRender::drawArraysInstanced: attribute-less instanced geometry, placed by the matrices the draw syncs. - al_add_test(alinstanceddraw_gl GL PROJECT llrender LIBRARIES llrender) - # ALGPUBuffer's store, and LLRender's indirect buffer bindings over its lifetime; the readback and stream rings. - al_add_test(algpubuffer_gl GL PROJECT llrender LIBRARIES llrender) - # ALImageSlot: what a unit issues, re-issues and forgets as LLRender keeps it, and integer images. - al_add_test(alimageslot_gl GL PROJECT llrender LIBRARIES llrender) endif() diff --git a/indra/llrender/tests/alfontcolrv1_gl_test.cpp b/indra/llrender/tests/alfontcolrv1_gl_test.cpp index 9312abb530..29caa226a4 100644 --- a/indra/llrender/tests/alfontcolrv1_gl_test.cpp +++ b/indra/llrender/tests/alfontcolrv1_gl_test.cpp @@ -36,15 +36,13 @@ #include "llfonttest_helpers.h" #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include #include #include -namespace tut -{ namespace { using namespace ll_test; @@ -62,91 +60,94 @@ namespace ll_test::FontStateScope font_scope; }; - typedef test_group alfontcolrv1_render_test; - typedef alfontcolrv1_render_test::object alfontcolrv1_render_object; - tut::alfontcolrv1_render_test alfontcolrv1_render_testcase("ALFontColrV1Render"); -} // End-to-end: requesting a Color glyph for the fire emoji on // Noto-COLRv1 must route through renderColrV1Glyph → painter → // setSubImageBGRA → Color atlas. Verify the returned info has // Color glyph type and the cache's Color page has a live GL // texture name. - template<> template<> - void alfontcolrv1_render_object::test<1>() + TEST_CASE_METHOD(alfontcolrv1_render_data, "ALFontColrV1Render: a Color fire emoji is painted onto a live Color atlas page", "[ALFontColrV1Render]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFtHead(path); - ensure("Noto-COLRv1 loaded as head", ft.notNull()); + UNSCOPED_INFO("Noto-COLRv1 loaded as head"); + REQUIRE(ft.notNull()); LLFontGlyphInfo* gi = ft->getGlyphInfo(0x1F525, EFontGlyphType::Color); - ensure("getGlyphInfo returned an entry for fire emoji", gi != nullptr); - ensure_equals("delivered glyph type is Color (COLRv1 raster)", - (S32)gi->mGlyphType, (S32)EFontGlyphType::Color); - ensure("glyph has positive width", gi->mWidth > 0); - ensure("glyph has positive height", gi->mHeight > 0); + UNSCOPED_INFO("getGlyphInfo returned an entry for fire emoji"); + REQUIRE(gi != nullptr); + UNSCOPED_INFO("delivered glyph type is Color (COLRv1 raster)"); + CHECK((S32)gi->mGlyphType == (S32)EFontGlyphType::Color); + UNSCOPED_INFO("glyph has positive width"); + CHECK(gi->mWidth > 0); + UNSCOPED_INFO("glyph has positive height"); + CHECK(gi->mHeight > 0); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - ensure("bitmap cache present", cache != nullptr); - ensure("Color atlas allocated at least one sheet", - cache->getNumBitmaps(EFontGlyphType::Color) >= 1u); + UNSCOPED_INFO("bitmap cache present"); + REQUIRE(cache != nullptr); + UNSCOPED_INFO("Color atlas allocated at least one sheet"); + CHECK(cache->getNumBitmaps(EFontGlyphType::Color) >= 1u); LLImageGL* page = cache->getImageGL(EFontGlyphType::Color, 0); - ensure("Color sheet 0 LLImageGL present", page != nullptr); - ensure("Color sheet 0 has a live GL texture name", - page->getTexName() != 0); + UNSCOPED_INFO("Color sheet 0 LLImageGL present"); + REQUIRE(page != nullptr); + UNSCOPED_INFO("Color sheet 0 has a live GL texture name"); + CHECK(page->getTexName() != 0); } // Several COLRv1 emojis in a row share the Color atlas — a single // 1024² Color sheet has plenty of room for many small color // bitmaps. Pins that the painter+atlas integration doesn't // allocate an extra sheet per glyph. - template<> template<> - void alfontcolrv1_render_object::test<2>() + TEST_CASE_METHOD(alfontcolrv1_render_data, "ALFontColrV1Render: three COLRv1 emoji share one Color atlas sheet", "[ALFontColrV1Render]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFtHead(path); - ensure("Noto-COLRv1 loaded as head", ft.notNull()); + UNSCOPED_INFO("Noto-COLRv1 loaded as head"); + REQUIRE(ft.notNull()); const llwchar fire = 0x1F525; const llwchar heart = 0x2764; const llwchar rocket = 0x1F680; - ensure("fire glyph allocated", - ft->getGlyphInfo(fire, EFontGlyphType::Color) != nullptr); - ensure("heart glyph allocated", - ft->getGlyphInfo(heart, EFontGlyphType::Color) != nullptr); - ensure("rocket glyph allocated", - ft->getGlyphInfo(rocket, EFontGlyphType::Color) != nullptr); + UNSCOPED_INFO("fire glyph allocated"); + REQUIRE(ft->getGlyphInfo(fire, EFontGlyphType::Color) != nullptr); + UNSCOPED_INFO("heart glyph allocated"); + REQUIRE(ft->getGlyphInfo(heart, EFontGlyphType::Color) != nullptr); + UNSCOPED_INFO("rocket glyph allocated"); + REQUIRE(ft->getGlyphInfo(rocket, EFontGlyphType::Color) != nullptr); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - ensure_equals("three Color glyphs share a single Color atlas sheet", - cache->getNumBitmaps(EFontGlyphType::Color), 1u); + UNSCOPED_INFO("three Color glyphs share a single Color atlas sheet"); + CHECK(cache->getNumBitmaps(EFontGlyphType::Color) == 1u); } // Repeat lookup is a cache hit, not a re-rasterize. Returning // the same LLFontGlyphInfo pointer pins the head's char_glyph // info cache (so each render call doesn't rebuild the painter // input on every frame). - template<> template<> - void alfontcolrv1_render_object::test<3>() + TEST_CASE_METHOD(alfontcolrv1_render_data, "ALFontColrV1Render: a repeat Color lookup returns the same cached LLFontGlyphInfo", "[ALFontColrV1Render]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFtHead(path); - ensure("Noto-COLRv1 loaded as head", ft.notNull()); + UNSCOPED_INFO("Noto-COLRv1 loaded as head"); + REQUIRE(ft.notNull()); LLFontGlyphInfo* first = ft->getGlyphInfo(0x1F525, EFontGlyphType::Color); LLFontGlyphInfo* second = ft->getGlyphInfo(0x1F525, EFontGlyphType::Color); - ensure("first lookup non-null", first != nullptr); - ensure("second lookup non-null", second != nullptr); - ensure_equals("repeat lookup returns same LLFontGlyphInfo pointer", - first, second); + UNSCOPED_INFO("first lookup non-null"); + REQUIRE(first != nullptr); + UNSCOPED_INFO("second lookup non-null"); + REQUIRE(second != nullptr); + UNSCOPED_INFO("repeat lookup returns same LLFontGlyphInfo pointer"); + REQUIRE(first == second); } // Color atlas generation counter advances on the first emoji @@ -154,24 +155,25 @@ namespace // buffer captured before the rasterize would invalidate. Pins // the global-generation contract that LLFontBitmapCache holds // for COLRv1 atlases too — not just Grayscale. - template<> template<> - void alfontcolrv1_render_object::test<4>() + TEST_CASE_METHOD(alfontcolrv1_render_data, "ALFontColrV1Render: the first Color raster advances the bitmap cache's generation", "[ALFontColrV1Render]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFtHead(path); - ensure("Noto-COLRv1 loaded as head", ft.notNull()); + UNSCOPED_INFO("Noto-COLRv1 loaded as head"); + REQUIRE(ft.notNull()); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); const S32 gen_before = cache->getCacheGeneration(); LLFontGlyphInfo* gi = ft->getGlyphInfo(0x1F525, EFontGlyphType::Color); - ensure("glyph allocated", gi != nullptr); + UNSCOPED_INFO("glyph allocated"); + REQUIRE(gi != nullptr); const S32 gen_after = cache->getCacheGeneration(); - ensure("cache generation advanced after Color rasterize", - gen_after > gen_before); + UNSCOPED_INFO("cache generation advanced after Color rasterize"); + CHECK(gen_after > gen_before); } // (Test 5 removed: pinned LLFontGlyphInfo::mTintWithForeground @@ -183,30 +185,31 @@ namespace // doesn't accumulate state across calls (e.g., a regression that // grew the staging buffer on every call would still produce a // valid bitmap but mWidth/mHeight could drift). - template<> template<> - void alfontcolrv1_render_object::test<6>() + TEST_CASE_METHOD(alfontcolrv1_render_data, "ALFontColrV1Render: one painter paints the same glyph twice at the same size", "[ALFontColrV1Render]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); hb_font_t* hb_font = face->getHbFont(); hb_codepoint_t fire = 0; hb_font_get_nominal_glyph(hb_font, 0x1F525, &fire); if (fire == 0) - skip("Noto-COLRv1 has no fire emoji"); + SKIP("Noto-COLRv1 has no fire emoji"); ALFontColrV1Painter painter; ALFontColrV1Painter::Result r1, r2; const LLColor4U fg(255, 255, 255, 255); - ensure("first paintGlyph succeeded", - painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r1)); + UNSCOPED_INFO("first paintGlyph succeeded"); + REQUIRE(painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r1)); const S32 w1 = r1.mWidth, h1 = r1.mHeight; - ensure("second paintGlyph (same glyph) succeeded", - painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r2)); - ensure_equals("repeat paint width matches", r2.mWidth, w1); - ensure_equals("repeat paint height matches", r2.mHeight, h1); + UNSCOPED_INFO("second paintGlyph (same glyph) succeeded"); + REQUIRE(painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r2)); + UNSCOPED_INFO("repeat paint width matches"); + CHECK(r2.mWidth == w1); + UNSCOPED_INFO("repeat paint height matches"); + CHECK(r2.mHeight == h1); } // Grayscale-COLRv1 routing: requesting EFontGlyphType::Grayscale on a @@ -215,30 +218,35 @@ namespace // change in renderGlyph drops the bitmap_type==Color restriction, so // Grayscale lookups on COLRv1 faces produce a luminance-shaded mask // in the LA atlas instead of an empty / outline-only glyph. - template<> template<> - void alfontcolrv1_render_object::test<7>() + TEST_CASE_METHOD(alfontcolrv1_render_data, "ALFontColrV1Render: a Grayscale lookup on a COLRv1 face paints into the Grayscale atlas", "[ALFontColrV1Render]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFtHead(path); - ensure("Noto-COLRv1 loaded as head", ft.notNull()); + UNSCOPED_INFO("Noto-COLRv1 loaded as head"); + REQUIRE(ft.notNull()); LLFontGlyphInfo* gi = ft->getGlyphInfo(0x1F525, EFontGlyphType::Grayscale); - ensure("Grayscale lookup returns an entry", gi != nullptr); - ensure_equals("delivered glyph type is Grayscale (LA atlas)", - (S32)gi->mGlyphType, (S32)EFontGlyphType::Grayscale); - ensure("Grayscale glyph has positive width", gi->mWidth > 0); - ensure("Grayscale glyph has positive height", gi->mHeight > 0); + UNSCOPED_INFO("Grayscale lookup returns an entry"); + REQUIRE(gi != nullptr); + UNSCOPED_INFO("delivered glyph type is Grayscale (LA atlas)"); + CHECK((S32)gi->mGlyphType == (S32)EFontGlyphType::Grayscale); + UNSCOPED_INFO("Grayscale glyph has positive width"); + CHECK(gi->mWidth > 0); + UNSCOPED_INFO("Grayscale glyph has positive height"); + CHECK(gi->mHeight > 0); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - ensure("bitmap cache present", cache != nullptr); - ensure("Grayscale atlas allocated at least one sheet", - cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); + UNSCOPED_INFO("bitmap cache present"); + REQUIRE(cache != nullptr); + UNSCOPED_INFO("Grayscale atlas allocated at least one sheet"); + CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); LLImageGL* page = cache->getImageGL(EFontGlyphType::Grayscale, 0); - ensure("Grayscale sheet 0 LLImageGL present", page != nullptr); - ensure("Grayscale sheet 0 has a live GL texture name", - page->getTexName() != 0); + UNSCOPED_INFO("Grayscale sheet 0 LLImageGL present"); + REQUIRE(page != nullptr); + UNSCOPED_INFO("Grayscale sheet 0 has a live GL texture name"); + CHECK(page->getTexName() != 0); } // Force-monochrome static is wired in at the LLFontGL render-call layer. @@ -247,33 +255,34 @@ namespace // verify that the Grayscale routing works while it's set, which is // what the call-site code does at llfontgl.cpp:560/715/862. Save and // restore the static so the test doesn't perturb sibling tests. - template<> template<> - void alfontcolrv1_render_object::test<8>() + TEST_CASE_METHOD(alfontcolrv1_render_data, "ALFontColrV1Render: with sForceMonochromeEmoji set the emoji is looked up as Grayscale", "[ALFontColrV1Render]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); const bool saved = LLFontGL::sForceMonochromeEmoji; LLFontGL::sForceMonochromeEmoji = true; LLPointer ft = loadFtHead(path); - ensure("Noto-COLRv1 loaded as head", ft.notNull()); + UNSCOPED_INFO("Noto-COLRv1 loaded as head"); + REQUIRE(ft.notNull()); // Simulate the render-call selector at llfontgl.cpp:560 with // use_color=true and force_mono=true → request Grayscale. const EFontGlyphType requested = (!/*use_color*/true || LLFontGL::sForceMonochromeEmoji) ? EFontGlyphType::Grayscale : EFontGlyphType::Color; - ensure_equals("force-mono toggle downgrades Color to Grayscale at " - "the request layer", (S32)requested, + UNSCOPED_INFO("force-mono toggle downgrades Color to Grayscale at " + "the request layer"); + CHECK((S32)requested == (S32)EFontGlyphType::Grayscale); LLFontGlyphInfo* gi = ft->getGlyphInfo(0x1F525, requested); - ensure("Grayscale lookup under force-mono returns an entry", - gi != nullptr); - ensure_equals("Grayscale path landed in Grayscale atlas", - (S32)gi->mGlyphType, (S32)EFontGlyphType::Grayscale); + UNSCOPED_INFO("Grayscale lookup under force-mono returns an entry"); + REQUIRE(gi != nullptr); + UNSCOPED_INFO("Grayscale path landed in Grayscale atlas"); + CHECK((S32)gi->mGlyphType == (S32)EFontGlyphType::Grayscale); LLFontGL::sForceMonochromeEmoji = saved; } diff --git a/indra/llrender/tests/alfontcolrv1_test.cpp b/indra/llrender/tests/alfontcolrv1_test.cpp index ba95c9af03..4b830a04e5 100644 --- a/indra/llrender/tests/alfontcolrv1_test.cpp +++ b/indra/llrender/tests/alfontcolrv1_test.cpp @@ -32,7 +32,7 @@ #include "llfonttest_helpers.h" -#include "../test/lltut.h" +#include #include #include @@ -61,8 +61,6 @@ namespace } } -namespace tut -{ namespace { using namespace ll_test; @@ -83,63 +81,67 @@ namespace } }; - typedef test_group alfontcolrv1_test; - typedef alfontcolrv1_test::object alfontcolrv1_object; - tut::alfontcolrv1_test alfontcolrv1_testcase("ALFontColrV1"); -} // Painter input validation: null hb_font fails cleanly without a crash. // Doesn't need a real font, so this test runs even when the test data // dir is empty (e.g., a stripped CI checkout). - template<> template<> - void alfontcolrv1_object::test<1>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: paintGlyph with a null hb_font returns false and an empty result", "[ALFontColrV1]") { ALFontColrV1Painter painter; ALFontColrV1Painter::Result result; const LLColor4U fg(255, 255, 255, 255); - ensure("paintGlyph(null hb_font) returns false", - !painter.paintGlyph(nullptr, /*glyph_index=*/0, + UNSCOPED_INFO("paintGlyph(null hb_font) returns false"); + REQUIRE_FALSE(painter.paintGlyph(nullptr, /*glyph_index=*/0, /*point_size=*/14.f, fg, /*palette_index=*/0, result)); - ensure("Result.mBitmap stays null on failure", - result.mBitmap == nullptr); - ensure_equals("Result.mWidth stays zero on failure", - result.mWidth, 0); + UNSCOPED_INFO("Result.mBitmap stays null on failure"); + REQUIRE(result.mBitmap == nullptr); + UNSCOPED_INFO("Result.mWidth stays zero on failure"); + CHECK(result.mWidth == 0); } // hasColrV1 is the in-tree probe for "FreeType's FT_LOAD_COLOR can't // rasterise this — route through the paint walker." Verify it fires // for Noto-COLRv1.ttf, which ships a COLRv1 paint table. - template<> template<> - void alfontcolrv1_object::test<2>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: hasColrV1 and hasColor are true for Noto-COLRv1", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) { - skip("Noto-COLRv1.ttf not present in test data dir"); + SKIP("Noto-COLRv1.ttf not present in test data dir"); } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("Noto-COLRv1 loaded", face.notNull() && face->isValid()); - ensure("Noto-COLRv1 hasColrV1 == true", face->hasColrV1()); - ensure("Noto-COLRv1 hasColor == true (general flag)", face->hasColor()); + { + INFO("Noto-COLRv1 loaded"); + REQUIRE(face.notNull()); + CHECK(face->isValid()); + } + UNSCOPED_INFO("Noto-COLRv1 hasColrV1 == true"); + CHECK(face->hasColrV1()); + UNSCOPED_INFO("Noto-COLRv1 hasColor == true (general flag)"); + CHECK(face->hasColor()); } // Negative: InterVariable.woff2 is a plain text font with no COLR // table, so hasColrV1 must stay false. Guards against the probe // false-positiving on any FT_HAS_COLOR-y face. - template<> template<> - void alfontcolrv1_object::test<3>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: hasColrV1 is false for a plain text face with no COLR table", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(path)) { - skip("InterVariable.woff2 not present in test data dir"); + SKIP("InterVariable.woff2 not present in test data dir"); } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("Inter loaded", face.notNull() && face->isValid()); - ensure("Inter hasColrV1 == false", !face->hasColrV1()); + { + INFO("Inter loaded"); + REQUIRE(face.notNull()); + CHECK(face->isValid()); + } + UNSCOPED_INFO("Inter hasColrV1 == false"); + CHECK_FALSE(face->hasColrV1()); } // Painter end-to-end: feed a known emoji glyph (fire, U+1F525) through @@ -147,66 +149,80 @@ namespace // one non-zero-alpha pixel. Also exercises the FT_Get_Color_Glyph_ClipBox // path (Noto-COLRv1 ships clip boxes for its glyphs) and the BGRA atlas // packing logic in paintGlyph. - template<> template<> - void alfontcolrv1_object::test<4>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: paintGlyph paints the fire emoji into a BGRA bitmap with some alpha", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) { - skip("Noto-COLRv1.ttf not present in test data dir"); + SKIP("Noto-COLRv1.ttf not present in test data dir"); } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("face loaded", face.notNull() && face->isValid()); - ensure("face has COLRv1", face->hasColrV1()); + { + INFO("face loaded"); + REQUIRE(face.notNull()); + CHECK(face->isValid()); + } + UNSCOPED_INFO("face has COLRv1"); + CHECK(face->hasColrV1()); hb_font_t* hb_font = face->getHbFont(); - ensure("hb_font available", hb_font != nullptr); + UNSCOPED_INFO("hb_font available"); + REQUIRE(hb_font != nullptr); // Resolve the fire emoji's glyph index via the cmap. Avoids hardcoding // a glyph_id that could shift across font versions. const hb_codepoint_t fire_cp = 0x1F525; hb_codepoint_t fire_gid = 0; - ensure("cmap maps U+1F525 to a glyph", - hb_font_get_nominal_glyph(hb_font, fire_cp, &fire_gid) - && fire_gid != 0); + { + INFO("cmap maps U+1F525 to a glyph"); + CHECK(hb_font_get_nominal_glyph(hb_font, fire_cp, &fire_gid)); + CHECK(fire_gid != 0); + } ALFontColrV1Painter painter; ALFontColrV1Painter::Result result; const LLColor4U fg(255, 255, 255, 255); const bool ok = painter.paintGlyph(hb_font, fire_gid, /*point_size=*/14.f, fg, /*palette_index=*/0, result); - ensure("paintGlyph succeeded", ok); - ensure("painter produced bitmap", - result.mBitmap != nullptr - && result.mWidth > 0 && result.mHeight > 0 - && result.mPitch != 0); - ensure("painter drew non-zero alpha somewhere", - anyAlpha(result.mBitmap, result.mWidth, result.mHeight, result.mPitch)); + UNSCOPED_INFO("paintGlyph succeeded"); + CHECK(ok); + { + INFO("painter produced bitmap"); + REQUIRE(result.mBitmap != nullptr); + CHECK(result.mWidth > 0); + CHECK(result.mHeight > 0); + CHECK(result.mPitch != 0); + } + UNSCOPED_INFO("painter drew non-zero alpha somewhere"); + CHECK(anyAlpha(result.mBitmap, result.mWidth, result.mHeight, result.mPitch)); } // VS-16 cmap absence is the trigger for shape_sub_run's stripping path. // Verify Noto-COLRv1 actually has the shape we expect: ZWJ in cmap but // VS-16 not. Other emoji fonts that ship VS-16 (e.g. Twemoji) take the // no-strip path; the shape pipeline branches on this flag per-face. - template<> template<> - void alfontcolrv1_object::test<5>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: Noto-COLRv1's cmap has ZWJ but no VS-16", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) { - skip("Noto-COLRv1.ttf not present in test data dir"); + SKIP("Noto-COLRv1.ttf not present in test data dir"); } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("face loaded", face.notNull() && face->isValid()); + { + INFO("face loaded"); + REQUIRE(face.notNull()); + CHECK(face->isValid()); + } // ZWJ should resolve to a real glyph (cmap entry present). - ensure("Noto cmap has ZWJ glyph", - face->getCharGlyphIndex(0x200D) != 0u); + UNSCOPED_INFO("Noto cmap has ZWJ glyph"); + CHECK(face->getCharGlyphIndex(0x200D) != 0u); // VS-16 should NOT — shape_sub_run keys VS-16 stripping on this. - ensure_equals("Noto cmap has no VS-16 glyph", - face->getCharGlyphIndex(0xFE0F), 0u); + UNSCOPED_INFO("Noto cmap has no VS-16 glyph"); + CHECK(face->getCharGlyphIndex(0xFE0F) == 0u); } // paintGlyph on an out-of-range glyph index (beyond the font's @@ -214,19 +230,23 @@ namespace // hb_font_paint_glyph which returns 0 for unknown glyphs; the // wrapper turns that into a Result with mBitmap=null and // mWidth=0. - template<> template<> - void alfontcolrv1_object::test<6>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: paintGlyph fails cleanly on a glyph index past the font's glyphs", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) { - skip("Noto-COLRv1.ttf not present in test data dir"); + SKIP("Noto-COLRv1.ttf not present in test data dir"); } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("face loaded", face.notNull() && face->isValid()); + { + INFO("face loaded"); + REQUIRE(face.notNull()); + CHECK(face->isValid()); + } hb_font_t* hb_font = face->getHbFont(); - ensure("hb_font available", hb_font != nullptr); + UNSCOPED_INFO("hb_font available"); + REQUIRE(hb_font != nullptr); ALFontColrV1Painter painter; ALFontColrV1Painter::Result result; @@ -237,12 +257,12 @@ namespace const bool ok = painter.paintGlyph(hb_font, /*glyph_index=*/0xFFFFFFFE, 14.f, fg, 0, result); - ensure("paintGlyph on out-of-range glyph index fails cleanly", - !ok); - ensure("result.mBitmap stays null on out-of-range", - result.mBitmap == nullptr); - ensure_equals("result.mWidth zero on out-of-range", - result.mWidth, 0); + UNSCOPED_INFO("paintGlyph on out-of-range glyph index fails cleanly"); + CHECK_FALSE(ok); + UNSCOPED_INFO("result.mBitmap stays null on out-of-range"); + REQUIRE(result.mBitmap == nullptr); + UNSCOPED_INFO("result.mWidth zero on out-of-range"); + CHECK(result.mWidth == 0); } // Painter staging surface reuse: paint two different emoji @@ -250,17 +270,20 @@ namespace // call's result must reflect the second glyph (not the first // — a staging-buffer leak would surface as the second result // pointing at stale first-glyph bytes when dimensions match). - template<> template<> - void alfontcolrv1_object::test<7>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: one painter paints two different emoji one after the other", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) { - skip("Noto-COLRv1.ttf not present in test data dir"); + SKIP("Noto-COLRv1.ttf not present in test data dir"); } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("face loaded", face.notNull() && face->isValid()); + { + INFO("face loaded"); + REQUIRE(face.notNull()); + CHECK(face->isValid()); + } hb_font_t* hb_font = face->getHbFont(); // Two distinct emoji: fire (U+1F525) and red heart (U+2764). @@ -275,19 +298,27 @@ namespace hb_codepoint_t heart = 0; hb_font_get_nominal_glyph(hb_font, 0x2764, &heart); if (fire == 0 || heart == 0) - skip("Noto-COLRv1 missing one of the test emoji codepoints"); + SKIP("Noto-COLRv1 missing one of the test emoji codepoints"); ALFontColrV1Painter painter; ALFontColrV1Painter::Result r1, r2; - ensure("first paintGlyph (fire) succeeded", - painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r1)); - ensure("first painter produced bitmap", - r1.mBitmap != nullptr && r1.mWidth > 0 && r1.mHeight > 0); - - ensure("second paintGlyph (heart) succeeded on same painter", - painter.paintGlyph(hb_font, heart, 14.f, fg, 0, r2)); - ensure("second painter produced bitmap", - r2.mBitmap != nullptr && r2.mWidth > 0 && r2.mHeight > 0); + UNSCOPED_INFO("first paintGlyph (fire) succeeded"); + REQUIRE(painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r1)); + { + INFO("first painter produced bitmap"); + REQUIRE(r1.mBitmap != nullptr); + CHECK(r1.mWidth > 0); + CHECK(r1.mHeight > 0); + } + + UNSCOPED_INFO("second paintGlyph (heart) succeeded on same painter"); + REQUIRE(painter.paintGlyph(hb_font, heart, 14.f, fg, 0, r2)); + { + INFO("second painter produced bitmap"); + REQUIRE(r2.mBitmap != nullptr); + CHECK(r2.mWidth > 0); + CHECK(r2.mHeight > 0); + } } // 2-em-square surface fallback (88585ff86d): a glyph with no @@ -301,30 +332,33 @@ namespace // also produces a sized bitmap. A negative version (synthetic // glyph forcing the fallback) requires a hand-built TTF and // is left for the integration suite. - template<> template<> - void alfontcolrv1_object::test<8>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: the fire emoji's surface is sized on the order of the point size", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) { - skip("Noto-COLRv1.ttf not present in test data dir"); + SKIP("Noto-COLRv1.ttf not present in test data dir"); } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("face loaded", face.notNull() && face->isValid()); + { + INFO("face loaded"); + REQUIRE(face.notNull()); + CHECK(face->isValid()); + } hb_font_t* hb_font = face->getHbFont(); hb_codepoint_t fire = 0; hb_font_get_nominal_glyph(hb_font, 0x1F525, &fire); if (fire == 0) - skip("Noto-COLRv1 has no fire emoji"); + SKIP("Noto-COLRv1 has no fire emoji"); constexpr F32 point_size = 14.f; ALFontColrV1Painter painter; ALFontColrV1Painter::Result result; const LLColor4U fg(255, 255, 255, 255); - ensure("paintGlyph succeeded", - painter.paintGlyph(hb_font, fire, point_size, fg, 0, result)); + UNSCOPED_INFO("paintGlyph succeeded"); + REQUIRE(painter.paintGlyph(hb_font, fire, point_size, fg, 0, result)); // Surface dimensions are on the order of point_size — the // ClipBox / FT-extents path produces a bitmap whose width and // height are within a small multiple of the requested point @@ -332,10 +366,16 @@ namespace // the lower bound; a regression that ballooned to 2× the // 2-em square (32×32 fallback for 14pt) would fail the upper // bound (~3× point_size). - ensure("surface width is in a reasonable range for 14pt", - result.mWidth >= 4 && result.mWidth <= (S32)(point_size * 4.f)); - ensure("surface height is in a reasonable range for 14pt", - result.mHeight >= 4 && result.mHeight <= (S32)(point_size * 4.f)); + { + INFO("surface width is in a reasonable range for 14pt"); + CHECK(result.mWidth >= 4); + CHECK(result.mWidth <= (S32)(point_size * 4.f)); + } + { + INFO("surface height is in a reasonable range for 14pt"); + CHECK(result.mHeight >= 4); + CHECK(result.mHeight <= (S32)(point_size * 4.f)); + } } // (Test 9 removed: pinned the mForegroundOnly flag on the painter @@ -347,19 +387,18 @@ namespace // gradient sampling used the same color for every offset) would // produce a single solid color → 1 distinct triple. Real gradient // emojis show many colors. Pin >= 3. - template<> template<> - void alfontcolrv1_object::test<10>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: the fire emoji's gradients paint at least three distinct colours", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); hb_font_t* hb_font = face->getHbFont(); hb_codepoint_t fire = 0; hb_font_get_nominal_glyph(hb_font, 0x1F525, &fire); if (fire == 0) - skip("Noto-COLRv1 has no fire emoji"); + SKIP("Noto-COLRv1 has no fire emoji"); ALFontColrV1Painter painter; ALFontColrV1Painter::Result result; @@ -368,10 +407,14 @@ namespace // them away. constexpr F32 ps = 32.f; const LLColor4U fg(255, 255, 255, 255); - ensure("paintGlyph succeeded", - painter.paintGlyph(hb_font, fire, ps, fg, 0, result)); - ensure("painter produced bitmap", - result.mBitmap != nullptr && result.mWidth > 0 && result.mHeight > 0); + UNSCOPED_INFO("paintGlyph succeeded"); + REQUIRE(painter.paintGlyph(hb_font, fire, ps, fg, 0, result)); + { + INFO("painter produced bitmap"); + REQUIRE(result.mBitmap != nullptr); + CHECK(result.mWidth > 0); + CHECK(result.mHeight > 0); + } std::set distinct; // packed BGR (alpha excluded) for (S32 y = 0; y < result.mHeight; ++y) @@ -389,8 +432,8 @@ namespace } // Fire emoji has at minimum a yellow body + orange gradient + // red highlights; >= 3 distinct triples is a conservative bound. - ensure("fire emoji produces >= 3 distinct RGB triples (gradient)", - distinct.size() >= 3); + UNSCOPED_INFO("fire emoji produces >= 3 distinct RGB triples (gradient)"); + REQUIRE(distinct.size() >= 3); } // palette_index argument propagates to hb_font_paint_glyph: paint @@ -399,31 +442,34 @@ namespace // differ at non-zero-alpha positions. Skip if only one palette is // present (no observable difference, but the code path is exercised // either way). - template<> template<> - void alfontcolrv1_object::test<11>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: palette_index reaches hb_font_paint_glyph: palette 1 paints differently", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); hb_font_t* hb_font = face->getHbFont(); hb_codepoint_t fire = 0; hb_font_get_nominal_glyph(hb_font, 0x1F525, &fire); if (fire == 0) - skip("Noto-COLRv1 has no fire emoji"); + SKIP("Noto-COLRv1 has no fire emoji"); const LLColor4U fg(255, 255, 255, 255); ALFontColrV1Painter painter_a, painter_b; ALFontColrV1Painter::Result r0, r1; - ensure("palette=0 paintGlyph succeeded", - painter_a.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/0, r0)); - ensure("palette=1 paintGlyph succeeded", - painter_b.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/1, r1)); - ensure("both produced bitmaps", - r0.mBitmap && r1.mBitmap - && r0.mWidth == r1.mWidth && r0.mHeight == r1.mHeight); + UNSCOPED_INFO("palette=0 paintGlyph succeeded"); + REQUIRE(painter_a.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/0, r0)); + UNSCOPED_INFO("palette=1 paintGlyph succeeded"); + REQUIRE(painter_b.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/1, r1)); + { + INFO("both produced bitmaps"); + REQUIRE(r0.mBitmap); + REQUIRE(r1.mBitmap); + CHECK(r0.mWidth == r1.mWidth); + CHECK(r0.mHeight == r1.mHeight); + } // Compare bytes — if the font has only one palette, hb resolves // both calls against palette 0 and the bitmaps match. Skip in @@ -445,44 +491,47 @@ namespace } } if (!any_diff) - skip("Noto-COLRv1 ships only one CPAL palette"); + SKIP("Noto-COLRv1 ships only one CPAL palette"); - ensure("palette swap changed at least one painted pixel", any_diff); + UNSCOPED_INFO("palette swap changed at least one painted pixel"); + CHECK(any_diff); } // OutputFormat::Gray basics: format selector hits the gray fold path // and returns a single-channel buffer with mPitch == width and // non-zero coverage somewhere in the bitmap. Pins the §1/§2 contract // that Gray output is single-channel, top-row first, tightly packed. - template<> template<> - void alfontcolrv1_object::test<12>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: OutputFormat::Gray paints a tightly packed one-channel bitmap", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); hb_font_t* hb_font = face->getHbFont(); hb_codepoint_t fire = 0; hb_font_get_nominal_glyph(hb_font, 0x1F525, &fire); if (fire == 0) - skip("Noto-COLRv1 has no fire emoji"); + SKIP("Noto-COLRv1 has no fire emoji"); ALFontColrV1Painter painter; ALFontColrV1Painter::Result result; const LLColor4U fg(255, 255, 255, 255); - ensure("paintGlyph(format=Gray) succeeded", - painter.paintGlyph(hb_font, fire, /*point_size=*/14.f, fg, + UNSCOPED_INFO("paintGlyph(format=Gray) succeeded"); + REQUIRE(painter.paintGlyph(hb_font, fire, /*point_size=*/14.f, fg, /*palette=*/0, ALFontColrV1Painter::OutputFormat::Gray, result)); - ensure_equals("Result.mFormat == Gray", - (S32)result.mFormat, + UNSCOPED_INFO("Result.mFormat == Gray"); + CHECK((S32)result.mFormat == (S32)ALFontColrV1Painter::OutputFormat::Gray); - ensure("Gray output has positive dims", - result.mWidth > 0 && result.mHeight > 0); - ensure_equals("Gray pitch is width (tightly packed single channel)", - result.mPitch, result.mWidth); + { + INFO("Gray output has positive dims"); + CHECK(result.mWidth > 0); + CHECK(result.mHeight > 0); + } + UNSCOPED_INFO("Gray pitch is width (tightly packed single channel)"); + CHECK(result.mPitch == result.mWidth); bool any_nonzero = false; for (S32 y = 0; y < result.mHeight && !any_nonzero; ++y) @@ -493,8 +542,8 @@ namespace if (row[x] != 0) { any_nonzero = true; break; } } } - ensure("Gray output has at least one non-zero coverage pixel", - any_nonzero); + UNSCOPED_INFO("Gray output has at least one non-zero coverage pixel"); + CHECK(any_nonzero); } // Gray-fold formula pin: paint the same glyph in BGRA and Gray and @@ -505,25 +554,24 @@ namespace // also verifies the foreground-collapses-to-alpha invariant since // Y == A and Y > A*0.15 holds, so the test is a strict superset of // the "fg-only collapses to alpha" case. - template<> template<> - void alfontcolrv1_object::test<13>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: each Gray byte is the hybrid luminance fold of the BGRA paint", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); hb_font_t* hb_font = face->getHbFont(); hb_codepoint_t fire = 0; hb_font_get_nominal_glyph(hb_font, 0x1F525, &fire); if (fire == 0) - skip("Noto-COLRv1 has no fire emoji"); + SKIP("Noto-COLRv1 has no fire emoji"); ALFontColrV1Painter painter; ALFontColrV1Painter::Result rgba_r, gray_r; const LLColor4U fg(255, 255, 255, 255); - ensure("BGRA paint succeeded", - painter.paintGlyph(hb_font, fire, 14.f, fg, 0, rgba_r)); + UNSCOPED_INFO("BGRA paint succeeded"); + REQUIRE(painter.paintGlyph(hb_font, fire, 14.f, fg, 0, rgba_r)); // Stash the BGRA bytes — the next paintGlyph call invalidates // mBitmap (it points back into mStaging which we overwrite below). std::vector bgra_bytes(rgba_r.mBitmap, @@ -531,12 +579,14 @@ namespace const S32 bgra_w = rgba_r.mWidth, bgra_h = rgba_r.mHeight; const S32 bgra_pitch = rgba_r.mPitch; - ensure("Gray paint succeeded", - painter.paintGlyph(hb_font, fire, 14.f, fg, 0, + UNSCOPED_INFO("Gray paint succeeded"); + REQUIRE(painter.paintGlyph(hb_font, fire, 14.f, fg, 0, ALFontColrV1Painter::OutputFormat::Gray, gray_r)); - ensure_equals("Gray and BGRA produce same width", gray_r.mWidth, bgra_w); - ensure_equals("Gray and BGRA produce same height", gray_r.mHeight, bgra_h); + UNSCOPED_INFO("Gray and BGRA produce same width"); + CHECK(gray_r.mWidth == bgra_w); + UNSCOPED_INFO("Gray and BGRA produce same height"); + CHECK(gray_r.mHeight == bgra_h); S32 mismatches = 0; for (S32 y = 0; y < bgra_h; ++y) @@ -559,8 +609,8 @@ namespace ++mismatches; } } - ensure_equals("every Gray byte matches the hybrid fold formula", - mismatches, 0); + UNSCOPED_INFO("every Gray byte matches the hybrid fold formula"); + CHECK(mismatches == 0); } // Hybrid floor pin: scan the BGRA result for fully-saturated palette @@ -570,12 +620,11 @@ namespace // still holds, just isn't observable here. The point of the test is // to lock the constant against future tweaks that would silently // crush dark CPAL details. - template<> template<> - void alfontcolrv1_object::test<14>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: Gray keeps an alpha floor under saturated black palette pixels", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); hb_font_t* hb_font = face->getHbFont(); @@ -620,49 +669,51 @@ namespace if (r == 0 && g == 0 && b == 0 && a > 200) { const U32 expected_floor = (a * 38u) >> 8; - ensure("Gray byte at saturated-black palette pixel " - "≥ alpha*floor (no crush to zero)", - (U32)gray_row[x] + 1u >= expected_floor); + UNSCOPED_INFO("Gray byte at saturated-black palette pixel " + "≥ alpha*floor (no crush to zero)"); + REQUIRE((U32)gray_row[x] + 1u >= expected_floor); return; } } } } - skip("no fully-saturated-black palette pixel found across test glyphs"); + SKIP("no fully-saturated-black palette pixel found across test glyphs"); } // Out-of-range palette_index clamps to 0 deterministically. Paint // fire with palette=0 and palette=99; the resulting bitmaps must // be byte-identical (the painter clamps any oversized index to 0 // before calling hb_font_paint_glyph). - template<> template<> - void alfontcolrv1_object::test<15>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: an out-of-range palette_index paints byte for byte as palette 0", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); hb_font_t* hb_font = face->getHbFont(); hb_codepoint_t fire = 0; hb_font_get_nominal_glyph(hb_font, 0x1F525, &fire); if (fire == 0) - skip("Noto-COLRv1 has no fire emoji"); + SKIP("Noto-COLRv1 has no fire emoji"); ALFontColrV1Painter painter_a, painter_b; ALFontColrV1Painter::Result r_zero, r_huge; const LLColor4U fg(255, 255, 255, 255); - ensure("palette=0 paint succeeded", - painter_a.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/0u, r_zero)); - ensure("palette=99 paint succeeded (clamps to 0)", - painter_b.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/99u, r_huge)); - ensure_equals("clamped result has same width", r_huge.mWidth, r_zero.mWidth); - ensure_equals("clamped result has same height", r_huge.mHeight, r_zero.mHeight); - ensure_equals("clamped result has same pitch", r_huge.mPitch, r_zero.mPitch); + UNSCOPED_INFO("palette=0 paint succeeded"); + REQUIRE(painter_a.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/0u, r_zero)); + UNSCOPED_INFO("palette=99 paint succeeded (clamps to 0)"); + REQUIRE(painter_b.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/99u, r_huge)); + UNSCOPED_INFO("clamped result has same width"); + CHECK(r_huge.mWidth == r_zero.mWidth); + UNSCOPED_INFO("clamped result has same height"); + CHECK(r_huge.mHeight == r_zero.mHeight); + UNSCOPED_INFO("clamped result has same pitch"); + CHECK(r_huge.mPitch == r_zero.mPitch); const size_t bytes = (size_t)r_zero.mPitch * r_zero.mHeight; - ensure("clamped result is byte-identical to palette=0", - std::memcmp(r_zero.mBitmap, r_huge.mBitmap, bytes) == 0); + UNSCOPED_INFO("clamped result is byte-identical to palette=0"); + CHECK(std::memcmp(r_zero.mBitmap, r_huge.mBitmap, bytes) == 0); } // Fallback point_size kicks in when the hb_font has ppem unset. @@ -671,26 +722,31 @@ namespace // bitmap rather than returning false. Specifically targets the // 2-em fallback inside the empty-bbox branch — without the // fallback point_size override, that branch would return false. - template<> template<> - void alfontcolrv1_object::test<16>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: with ppem unset paintGlyph sizes from the fallback point size or fails", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); // Build a dedicated hb_font on top of the same FT_Face so we can // mutate its ppem without affecting the face cache shared with // other tests in this group. hb_font_t* shared = face->getHbFont(); - ensure("shared hb_font available", shared != nullptr); + UNSCOPED_INFO("shared hb_font available"); + REQUIRE(shared != nullptr); hb_face_t* hb_face = hb_font_get_face(shared); hb_font_t* hb_font = hb_font_create(hb_face); - ensure("derived hb_font allocated", hb_font != nullptr); + UNSCOPED_INFO("derived hb_font allocated"); + REQUIRE(hb_font != nullptr); hb_font_set_ppem(hb_font, 0, 0); unsigned px = 1, py = 1; hb_font_get_ppem(hb_font, &px, &py); - ensure("ppem really is unset on derived font", px == 0 && py == 0); + { + INFO("ppem really is unset on derived font"); + CHECK(px == 0); + CHECK(py == 0); + } // Pick a glyph by index — without a scale set, the cmap query // through hb_font_get_nominal_glyph may not work as expected, so @@ -701,7 +757,7 @@ namespace if (fire == 0) { hb_font_destroy(hb_font); - skip("Noto-COLRv1 has no fire emoji"); + SKIP("Noto-COLRv1 has no fire emoji"); } ALFontColrV1Painter painter; @@ -720,8 +776,11 @@ namespace // failure mode we're protecting against. if (ok) { - ensure("fallback path produced a sized bitmap", - result.mWidth > 0 && result.mHeight > 0); + { + INFO("fallback path produced a sized bitmap"); + CHECK(result.mWidth > 0); + CHECK(result.mHeight > 0); + } } hb_font_destroy(hb_font); } @@ -732,43 +791,50 @@ namespace // post-transform paint commands. A regression that drops PAD back to // 1 (or below) would surface here as a fringe of non-zero pixels at // the buffer boundary that visually clips the glyph at the atlas. - template<> template<> - void alfontcolrv1_object::test<17>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: a painted glyph's outermost rows and columns have zero alpha", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); hb_font_t* hb_font = face->getHbFont(); hb_codepoint_t fire = 0; hb_font_get_nominal_glyph(hb_font, 0x1F525, &fire); if (fire == 0) - skip("Noto-COLRv1 has no fire emoji"); + SKIP("Noto-COLRv1 has no fire emoji"); ALFontColrV1Painter painter; ALFontColrV1Painter::Result r; const LLColor4U fg(255, 255, 255, 255); - ensure("paintGlyph succeeded", - painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r)); - ensure("bitmap valid", - r.mBitmap && r.mWidth >= 4 && r.mHeight >= 4); + UNSCOPED_INFO("paintGlyph succeeded"); + REQUIRE(painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r)); + { + INFO("bitmap valid"); + REQUIRE(r.mBitmap); + CHECK(r.mWidth >= 4); + CHECK(r.mHeight >= 4); + } // First row, last row. for (S32 x = 0; x < r.mWidth; ++x) { const U8* top = r.mBitmap + x*4; const U8* bottom = r.mBitmap + (ptrdiff_t)(r.mHeight-1)*r.mPitch + x*4; - ensure_equals("top row alpha is zero", (S32)top[3], 0); - ensure_equals("bottom row alpha is zero", (S32)bottom[3], 0); + UNSCOPED_INFO("top row alpha is zero"); + REQUIRE((S32)top[3] == 0); + UNSCOPED_INFO("bottom row alpha is zero"); + REQUIRE((S32)bottom[3] == 0); } // First column, last column. for (S32 y = 0; y < r.mHeight; ++y) { const U8* left = r.mBitmap + (ptrdiff_t)y*r.mPitch + 0*4; const U8* right = r.mBitmap + (ptrdiff_t)y*r.mPitch + (r.mWidth-1)*4; - ensure_equals("left col alpha is zero", (S32)left[3], 0); - ensure_equals("right col alpha is zero", (S32)right[3], 0); + UNSCOPED_INFO("left col alpha is zero"); + REQUIRE((S32)left[3] == 0); + UNSCOPED_INFO("right col alpha is zero"); + REQUIRE((S32)right[3] == 0); } } } diff --git a/indra/llrender/tests/alfontshaping_gl_test.cpp b/indra/llrender/tests/alfontshaping_gl_test.cpp index 5d9bd95e1e..c6691d199d 100644 --- a/indra/llrender/tests/alfontshaping_gl_test.cpp +++ b/indra/llrender/tests/alfontshaping_gl_test.cpp @@ -33,7 +33,7 @@ #include "llfonttest_helpers.h" #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include @@ -41,8 +41,6 @@ #include #include -namespace tut -{ namespace { using namespace ll_test; @@ -58,10 +56,6 @@ namespace ll_test::FontStateScope font_scope; }; - typedef test_group alfontshaping_gl_test; - typedef alfontshaping_gl_test::object alfontshaping_gl_object; - tut::alfontshaping_gl_test alfontshaping_gl_testcase("ALFontShapingGL"); -} // Strict-monospace (ligatures off): shape "AB" through DejaVuSansMono. // Routes through HB with the kFixedWidthStrict feature plan @@ -69,65 +63,73 @@ namespace // produces one glyph per codepoint with bit-exact FT mXAdvance and // zero positioning offsets — same contract the retired bypass // enforced. Pins the cell-alignment invariant. - template<> template<> - void alfontshaping_gl_object::test<1>() + TEST_CASE_METHOD(alfontshaping_gl_data, "ALFontShapingGL: strict monospace shapes AB to equal FT cell advances and no offsets", "[ALFontShapingGL]") { const std::string path = std::string(kFontDir) + "DejaVuSansMono.woff2"; if (!fileExists(path)) - skip("DejaVuSansMono.woff2 not present"); + SKIP("DejaVuSansMono.woff2 not present"); LLPointer ft = loadFtHead(path); - ensure("DejaVuSansMono loaded", ft.notNull()); - ensure("DejaVuSansMono is fixed-width", ft->isFixedWidth()); - ensure("monospace ligatures default off", - !ft->getAllowMonospaceLigatures()); + UNSCOPED_INFO("DejaVuSansMono loaded"); + REQUIRE(ft.notNull()); + UNSCOPED_INFO("DejaVuSansMono is fixed-width"); + CHECK(ft->isFixedWidth()); + UNSCOPED_INFO("monospace ligatures default off"); + CHECK_FALSE(ft->getAllowMonospaceLigatures()); const Text s = text('A','B'); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure_equals("AB produces 2 glyphs through HB strict-mono path", - out.size(), 2u); - ensure("each shaped glyph has positive advance", - out[0].x_advance > 0.f && out[1].x_advance > 0.f); - ensure_equals("strict-mono: A and B advances are equal", - out[0].x_advance, out[1].x_advance); + UNSCOPED_INFO("AB produces 2 glyphs through HB strict-mono path"); + REQUIRE(out.size() == 2u); + { + INFO("each shaped glyph has positive advance"); + CHECK(out[0].x_advance > 0.f); + CHECK(out[1].x_advance > 0.f); + } + UNSCOPED_INFO("strict-mono: A and B advances are equal"); + CHECK(out[0].x_advance == out[1].x_advance); // HB output for monospace ASCII matches FT mXAdvance bit-exact // under the strict feature plan (verified by the long-line // probe in test 20). - ensure_equals("strict-mono glyph[0] advance == ft->getXAdvance('A')", - out[0].x_advance, ft->getXAdvance(L'A')); + UNSCOPED_INFO("strict-mono glyph[0] advance == ft->getXAdvance('A')"); + CHECK(out[0].x_advance == ft->getXAdvance(L'A')); // Strict-mono path also forces zero positioning offsets, since // monospace ASCII triggers no GPOS adjustments under the // feature plan (kern, mark, mkmk all suppressed for ASCII). - ensure_equals("strict-mono glyph[0] x_offset is zero", - out[0].x_offset, 0.f); - ensure_equals("strict-mono glyph[0] y_offset is zero", - out[0].y_offset, 0.f); + UNSCOPED_INFO("strict-mono glyph[0] x_offset is zero"); + CHECK(out[0].x_offset == 0.f); + UNSCOPED_INFO("strict-mono glyph[0] y_offset is zero"); + CHECK(out[0].y_offset == 0.f); } // Programmer-mono opt-in via setAllowMonospaceLigatures(true). // Routes through HB with the kFixedWidthLigaturesOk feature plan // (kern off, ligatures allowed). Cell alignment invariant on the // pre-ligation columns still holds. - template<> template<> - void alfontshaping_gl_object::test<2>() + TEST_CASE_METHOD(alfontshaping_gl_data, "ALFontShapingGL: monospace with ligatures allowed keeps AV on equal cell advances", "[ALFontShapingGL]") { const std::string path = std::string(kFontDir) + "DejaVuSansMono.woff2"; if (!fileExists(path)) - skip("DejaVuSansMono.woff2 not present"); + SKIP("DejaVuSansMono.woff2 not present"); LLPointer ft = loadFtHead(path); - ensure("DejaVuSansMono loaded", ft.notNull()); + UNSCOPED_INFO("DejaVuSansMono loaded"); + REQUIRE(ft.notNull()); ft->setAllowMonospaceLigatures(true); - ensure("ligatures-on toggle applied", - ft->getAllowMonospaceLigatures()); + UNSCOPED_INFO("ligatures-on toggle applied"); + CHECK(ft->getAllowMonospaceLigatures()); const Text s = text('A','V'); // AV is a classic kerned pair std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure_equals("AV produces 2 glyphs through HB", out.size(), 2u); - ensure("each shaped glyph has positive advance", - out[0].x_advance > 0.f && out[1].x_advance > 0.f); - ensure_equals("HB-monospace-with-ligatures: A and V advances are equal", - out[0].x_advance, out[1].x_advance); + UNSCOPED_INFO("AV produces 2 glyphs through HB"); + REQUIRE(out.size() == 2u); + { + INFO("each shaped glyph has positive advance"); + CHECK(out[0].x_advance > 0.f); + CHECK(out[1].x_advance > 0.f); + } + UNSCOPED_INFO("HB-monospace-with-ligatures: A and V advances are equal"); + CHECK(out[0].x_advance == out[1].x_advance); } // HB GPOS plumbing: shape "AV" (classic Latin kerned pair) through @@ -137,14 +139,14 @@ namespace // advances. A regression that disabled GPOS unconditionally would // make these equal. Skip if no Latin kern pair fires (test // verifies nothing then). - template<> template<> - void alfontshaping_gl_object::test<3>() + TEST_CASE_METHOD(alfontshaping_gl_data, "ALFontShapingGL: GPOS kerning reaches HB shaping for some Latin pair in DejaVuSans", "[ALFontShapingGL]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFtHead(path); - ensure("DejaVuSans loaded", ft.notNull()); + UNSCOPED_INFO("DejaVuSans loaded"); + REQUIRE(ft.notNull()); // Try a handful of classic kerned pairs; pick the first that // shows a measurable kern. DejaVu's GPOS coverage varies. @@ -177,9 +179,9 @@ namespace } } if (!found_kerned) - skip("DejaVuSans lacks a measurable Latin kern pair"); + SKIP("DejaVuSans lacks a measurable Latin kern pair"); - ensure("at least one Latin kern pair fired through HB GPOS", - found_kerned); + UNSCOPED_INFO("at least one Latin kern pair fired through HB GPOS"); + CHECK(found_kerned); } } diff --git a/indra/llrender/tests/alfontshaping_test.cpp b/indra/llrender/tests/alfontshaping_test.cpp index 5852663895..b937526501 100644 --- a/indra/llrender/tests/alfontshaping_test.cpp +++ b/indra/llrender/tests/alfontshaping_test.cpp @@ -37,7 +37,7 @@ #include "llfonttest_helpers.h" -#include "../test/lltut.h" +#include #include #include @@ -60,8 +60,6 @@ // stem-darkening test below can FT_Property_Get against it. extern FT_Library gFTLibrary; -namespace tut -{ namespace { using namespace ll_test; @@ -85,37 +83,37 @@ namespace } }; - typedef test_group alfontshaping_test; - typedef alfontshaping_test::object alfontshaping_object; - tut::alfontshaping_test alfontshaping_testcase("ALFontShaping"); -} // Null root face and empty/inverted ranges all produce empty // output. The header documents these as the safe-fallback // contracts callers rely on for "fall back to 1:1 codepoint path." - template<> template<> - void alfontshaping_object::test<1>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a null face or an empty or inverted or overlong slice shapes nothing", "[ALFontShaping]") { std::vector out; const Text s = text('a','b','c'); ALFontShaping::shapeRun(/*root_face=*/nullptr, s, 0, s.size(), out); - ensure("null root face -> empty output", out.empty()); + UNSCOPED_INFO("null root face -> empty output"); + REQUIRE(out.empty()); const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + UNSCOPED_INFO("DejaVuSans loaded"); + REQUIRE(ft.notNull()); ALFontShaping::shapeRun(ft, s, /*begin=*/2, /*end=*/2, out); - ensure("begin == end -> empty output", out.empty()); + UNSCOPED_INFO("begin == end -> empty output"); + REQUIRE(out.empty()); ALFontShaping::shapeRun(ft, s, /*begin=*/3, /*end=*/2, out); - ensure("begin > end -> empty output", out.empty()); + UNSCOPED_INFO("begin > end -> empty output"); + REQUIRE(out.empty()); ALFontShaping::shapeRun(ft, s, /*begin=*/0, /*end=*/99, out); - ensure("end > size -> empty output", out.empty()); + UNSCOPED_INFO("end > size -> empty output"); + REQUIRE(out.empty()); } // ASCII through a proportional Latin font: 1 glyph per codepoint, @@ -130,66 +128,71 @@ namespace // → gGL.bind, which a pure-CPU test binary can't satisfy. The cmap // match below is the strongest identity check we can make without // rasterizing. - template<> template<> - void alfontshaping_object::test<2>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: ASCII shapes to one glyph per codepoint with its cmap id and an advance", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + UNSCOPED_INFO("DejaVuSans loaded"); + REQUIRE(ft.notNull()); const Text s = text('a','b','c'); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure_equals("3 glyphs for 'abc'", out.size(), 3u); + UNSCOPED_INFO("3 glyphs for 'abc'"); + REQUIRE(out.size() == 3u); for (const auto& g : out) { - ensure("each glyph has a face", g.face != nullptr); - ensure("each glyph has a glyph_id", g.glyph_id != 0u); - ensure("each glyph has a positive x_advance", - g.x_advance > 0.f); + UNSCOPED_INFO("each glyph has a face"); + REQUIRE(g.face != nullptr); + UNSCOPED_INFO("each glyph has a glyph_id"); + REQUIRE(g.glyph_id != 0u); + UNSCOPED_INFO("each glyph has a positive x_advance"); + REQUIRE(g.x_advance > 0.f); } // Identity check: the shape's glyph_id for each ASCII char // must match the face's cmap-direct lookup. A regression that // bound HB to the wrong face would produce ids that disagree. - ensure_equals("glyph[0] id matches cmap('a')", - out[0].glyph_id, ft->getCharGlyphIndex(L'a')); - ensure_equals("glyph[1] id matches cmap('b')", - out[1].glyph_id, ft->getCharGlyphIndex(L'b')); - ensure_equals("glyph[2] id matches cmap('c')", - out[2].glyph_id, ft->getCharGlyphIndex(L'c')); + UNSCOPED_INFO("glyph[0] id matches cmap('a')"); + CHECK(out[0].glyph_id == ft->getCharGlyphIndex(L'a')); + UNSCOPED_INFO("glyph[1] id matches cmap('b')"); + CHECK(out[1].glyph_id == ft->getCharGlyphIndex(L'b')); + UNSCOPED_INFO("glyph[2] id matches cmap('c')"); + CHECK(out[2].glyph_id == ft->getCharGlyphIndex(L'c')); } // shapeRun rebases cluster indices to the original wstr; // shapeLine returns slice-local clusters. Pin both contracts — // the renderer hot path branches on this distinction // (alfontshaping.h:80-90). - template<> template<> - void alfontshaping_object::test<3>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: shapeRun clusters index the whole string and shapeLine's the slice", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + UNSCOPED_INFO("DejaVuSans loaded"); + REQUIRE(ft.notNull()); const Text s = text('X','Y','a','b','c'); const size_t begin = s.at(2); const size_t end = s.at(5); std::vector shape_run_out; ALFontShaping::shapeRun(ft, s, begin, end, shape_run_out); - ensure_equals("shapeRun produced 3 glyphs", shape_run_out.size(), 3u); + UNSCOPED_INFO("shapeRun produced 3 glyphs"); + REQUIRE(shape_run_out.size() == 3u); // 'a' starts where the slice does. - ensure_equals("shapeRun cluster[0] is an original-string offset", - shape_run_out[0].cluster, (S32)begin); + UNSCOPED_INFO("shapeRun cluster[0] is an original-string offset"); + CHECK(shape_run_out[0].cluster == (S32)begin); const auto& shape_line_out = ALFontShaping::shapeLine(ft, s, begin, end); - ensure_equals("shapeLine produced 3 glyphs", shape_line_out.size(), 3u); + UNSCOPED_INFO("shapeLine produced 3 glyphs"); + REQUIRE(shape_line_out.size() == 3u); // shapeLine clusters are slice-local: 'a' is at slice offset 0. - ensure_equals("shapeLine cluster[0] is slice-local offset 0", - shape_line_out[0].cluster, 0); + UNSCOPED_INFO("shapeLine cluster[0] is slice-local offset 0"); + CHECK(shape_line_out[0].cluster == 0); } // Cache hit contract: shaping the same (slice, root_face) twice @@ -197,14 +200,14 @@ namespace // This is what callers depend on across frames (pen positions and // glyph ids must not drift), and it's the load-bearing invariant // for the LRU's reuse semantics. - template<> template<> - void alfontshaping_object::test<4>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: shaping the same slice twice gives a bit-identical glyph stream", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + UNSCOPED_INFO("DejaVuSans loaded"); + REQUIRE(ft.notNull()); const Text s = text('h','e','l','l','o'); std::vector first; @@ -212,16 +215,16 @@ namespace std::vector second; ALFontShaping::shapeRun(ft, s, 0, s.size(), second); - ensure_equals("cache hit produces same glyph count", - second.size(), first.size()); + UNSCOPED_INFO("cache hit produces same glyph count"); + REQUIRE(second.size() == first.size()); for (size_t i = 0; i < first.size(); ++i) { - ensure_equals("cache hit: same glyph_id", - second[i].glyph_id, first[i].glyph_id); - ensure_equals("cache hit: same cluster", - second[i].cluster, first[i].cluster); - ensure_equals("cache hit: same x_advance", - second[i].x_advance, first[i].x_advance); + UNSCOPED_INFO("cache hit: same glyph_id"); + REQUIRE(second[i].glyph_id == first[i].glyph_id); + UNSCOPED_INFO("cache hit: same cluster"); + REQUIRE(second[i].cluster == first[i].cluster); + UNSCOPED_INFO("cache hit: same x_advance"); + REQUIRE(second[i].x_advance == first[i].x_advance); } } @@ -234,30 +237,35 @@ namespace // Without instrumentation we observe "cache cleared" indirectly // by checking the returned reference identity from shapeLine — // a cleared entry rebuilds to a fresh storage location. - template<> template<> - void alfontshaping_object::test<5>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: clearCacheForFace on one face leaves another face's entry in place", "[ALFontShaping]") { const std::string a_path = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string b_path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(a_path) || !fileExists(b_path)) - skip("DejaVuSans + InterVariable both required"); + SKIP("DejaVuSans + InterVariable both required"); LLPointer a = loadFt(a_path); LLPointer b = loadFt(b_path); - ensure("both faces loaded", a.notNull() && b.notNull()); + { + INFO("both faces loaded"); + REQUIRE(a.notNull()); + REQUIRE(b.notNull()); + } const Text s = text('a','b'); const auto& a_first = ALFontShaping::shapeLine(a, s, 0, s.size()); const auto& b_first = ALFontShaping::shapeLine(b, s, 0, s.size()); - ensure("a shape non-empty", !a_first.empty()); - ensure("b shape non-empty", !b_first.empty()); + UNSCOPED_INFO("a shape non-empty"); + REQUIRE_FALSE(a_first.empty()); + UNSCOPED_INFO("b shape non-empty"); + REQUIRE_FALSE(b_first.empty()); const void* a_addr0 = a_first.data(); const void* b_addr0 = b_first.data(); // Hit A again: cache hit returns same storage address. const auto& a_second = ALFontShaping::shapeLine(a, s, 0, s.size()); - ensure_equals("A cache hit: same backing storage", - a_second.data(), a_addr0); + UNSCOPED_INFO("A cache hit: same backing storage"); + REQUIRE(a_second.data() == a_addr0); ALFontShaping::clearCacheForFace(a); @@ -266,53 +274,55 @@ namespace // address by the allocator after clear is implementation- // defined and not part of the contract.) const auto& a_after = ALFontShaping::shapeLine(a, s, 0, s.size()); - ensure("A re-shape produces output", !a_after.empty()); + UNSCOPED_INFO("A re-shape produces output"); + REQUIRE_FALSE(a_after.empty()); const auto& b_after = ALFontShaping::shapeLine(b, s, 0, s.size()); - ensure_equals("B cache survived clearCacheForFace(A)", - b_after.data(), b_addr0); + UNSCOPED_INFO("B cache survived clearCacheForFace(A)"); + REQUIRE(b_after.data() == b_addr0); } // ZWJ heart-on-fire: U+2764 U+200D U+1F525 must collapse to a // single glyph through Noto-COLRv1 (whose GSUB has the ligature // rule). Pins d4a5a4c904 — the regression that left ZWJ sequences // unligatured because the run was fragmenting at VS-16 and ZWJ. - template<> template<> - void alfontshaping_object::test<6>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: the ZWJ heart-on-fire ligates to one glyph through Noto-COLRv1", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFt(path); - ensure("Noto-COLRv1 loaded", ft.notNull()); + UNSCOPED_INFO("Noto-COLRv1 loaded"); + REQUIRE(ft.notNull()); // U+2764 (heart) U+200D (ZWJ) U+1F525 (fire) const Text s = text(0x2764, 0x200D, 0x1F525); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure("heart-on-fire shaped to non-empty output", !out.empty()); - ensure_equals("heart-on-fire ZWJ ligated to a single glyph", - out.size(), 1u); + UNSCOPED_INFO("heart-on-fire shaped to non-empty output"); + REQUIRE_FALSE(out.empty()); + UNSCOPED_INFO("heart-on-fire ZWJ ligated to a single glyph"); + REQUIRE(out.size() == 1u); } // VS-16 strip path: shaping U+2764 U+FE0F through Noto-COLRv1 // (whose cmap lacks U+FE0F) must produce the same output as // shaping U+2764 alone — shape_sub_run pre-strips VS-16 before // passing to HB. Pins alfontshaping.cpp:257-264. - template<> template<> - void alfontshaping_object::test<7>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a VS-16 the cmap lacks is stripped: same glyphs as the base alone", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFt(path); - ensure("Noto-COLRv1 loaded", ft.notNull()); + UNSCOPED_INFO("Noto-COLRv1 loaded"); + REQUIRE(ft.notNull()); // Sanity: confirm VS-16 is genuinely absent from this font's // cmap (otherwise the strip path doesn't fire and the test // verifies nothing useful). - ensure_equals("Noto-COLRv1 cmap lacks U+FE0F", - ft->getCharGlyphIndex(0xFE0F), 0u); + UNSCOPED_INFO("Noto-COLRv1 cmap lacks U+FE0F"); + CHECK(ft->getCharGlyphIndex(0xFE0F) == 0u); const Text with_vs = text(0x2764, 0xFE0F); const Text without_vs = text(0x2764); @@ -322,34 +332,94 @@ namespace ALFontShaping::shapeRun(ft, with_vs, 0, with_vs.size(), with_out); ALFontShaping::shapeRun(ft, without_vs, 0, without_vs.size(), without_out); - ensure_equals("with-VS and without-VS produce same glyph count", - with_out.size(), without_out.size()); - ensure("with-VS produced at least one glyph", !with_out.empty()); + UNSCOPED_INFO("with-VS and without-VS produce same glyph count"); + REQUIRE(with_out.size() == without_out.size()); + UNSCOPED_INFO("with-VS produced at least one glyph"); + REQUIRE_FALSE(with_out.empty()); for (size_t i = 0; i < with_out.size(); ++i) { - ensure_equals("with-VS glyph_id matches without-VS", - with_out[i].glyph_id, without_out[i].glyph_id); + UNSCOPED_INFO("with-VS glyph_id matches without-VS"); + REQUIRE(with_out[i].glyph_id == without_out[i].glyph_id); } } + // clearCache empties the LRU; subsequent shapes repopulate it. The + // earlier version of this test compared the returned reference's + // data() pointer before vs after clear and asserted the address + // changed — that was allocator-implementation-defined. Modern + // allocators (LFH on Windows, jemalloc, mimalloc, ...) recycle freed + // slots LIFO, so the rebuilt entry's vector storage routinely lands + // at the just-freed address and the assertion flaked ~3-of-5 runs. + // cacheSize() pins the contract the cache actually guarantees + // (count goes to 0 then back to 1) without depending on heap + // recycling behavior. + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: clearCache empties the LRU; subsequent shapes repopulate it", "[ALFontShaping]") + { + const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; + if (!fileExists(path)) + SKIP("DejaVuSans.woff2 not present"); + LLPointer ft = loadFt(path); + UNSCOPED_INFO("DejaVuSans loaded"); + REQUIRE(ft.notNull()); + + const Text s = text('h','i'); + const auto& first = ALFontShaping::shapeLine(ft, s, 0, s.size()); + UNSCOPED_INFO("shape produced output"); + REQUIRE_FALSE(first.empty()); + UNSCOPED_INFO("first shape leaves one entry"); + CHECK(ALFontShaping::cacheSize() == 1u); + // Snapshot the size BEFORE clearCache invalidates `first` — + // shapeLine returns a reference into the LRU, so the entry's + // storage is gone the moment the LRU is cleared. Reading + // first.size() afterward would be a use-after-free. + const size_t first_size = first.size(); + const void* addr0 = first.data(); + + // Cache hit: same storage AND the entry count stays at one. + const auto& second = ALFontShaping::shapeLine(ft, s, 0, s.size()); + UNSCOPED_INFO("cache hit storage is stable"); + REQUIRE(second.data() == addr0); + UNSCOPED_INFO("cache hit doesn't add an entry"); + CHECK(ALFontShaping::cacheSize() == 1u); + + ALFontShaping::clearCache(); + UNSCOPED_INFO("clearCache empties the LRU"); + CHECK(ALFontShaping::cacheSize() == 0u); + + // Miss: re-shape repopulates the cache. Output content is + // identical (shaping is deterministic given the same face + + // codepoints); the cache state is what changed. + const auto& after = ALFontShaping::shapeLine(ft, s, 0, s.size()); + UNSCOPED_INFO("post-clear rebuild produced output"); + REQUIRE_FALSE(after.empty()); + UNSCOPED_INFO("post-clear miss repopulated to one entry"); + CHECK(ALFontShaping::cacheSize() == 1u); + UNSCOPED_INFO("post-clear shape is content-identical"); + REQUIRE(after.size() == first_size); + } + // Multi-hop shape run: ASCII + CJK + Emoji through a head (DejaVu) // with two fallbacks (SourceHanSans + Noto-COLRv1). shape_sub_run // itemizes by face coverage so each cluster routes to the right // face. Pins gap audit #3 — multi-hop selectShapingFace was // untested, the existing fallback tests use a single hop only. - template<> template<> - void alfontshaping_object::test<9>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: ASCII then CJK then emoji route to the head and each of two fallbacks", "[ALFontShaping]") { const std::string dejavu = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string han = std::string(kFontDir) + "SourceHanSans-Regular.woff2"; const std::string emoji = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(dejavu) || !fileExists(han) || !fileExists(emoji)) - skip("DejaVuSans + SourceHanSans + Noto-COLRv1 required"); + SKIP("DejaVuSans + SourceHanSans + Noto-COLRv1 required"); LLPointer head = loadFt(dejavu); LLPointer cjk = loadFt(han); LLPointer emo = loadFt(emoji); - ensure("all loaded", head.notNull() && cjk.notNull() && emo.notNull()); + { + INFO("all loaded"); + REQUIRE(head.notNull()); + REQUIRE(cjk.notNull()); + REQUIRE(emo.notNull()); + } head->addFallbackFont(cjk); head->addFallbackFont(emo); @@ -357,16 +427,23 @@ namespace const Text s = text('a', 0x4F60, 0x1F525); std::vector out; ALFontShaping::shapeRun(head, s, 0, s.size(), out); - ensure_equals("3 glyphs (one per codepoint)", out.size(), 3u); + UNSCOPED_INFO("3 glyphs (one per codepoint)"); + REQUIRE(out.size() == 3u); // Each glyph carries the face it came from — verify the routing. - ensure_equals("'a' routed to head (DejaVu)", out[0].face, head.get()); - ensure_equals("CJK routed to SourceHanSans", out[1].face, cjk.get()); - ensure_equals("emoji routed to Noto-COLRv1", out[2].face, emo.get()); + UNSCOPED_INFO("'a' routed to head (DejaVu)"); + REQUIRE(out[0].face == head.get()); + UNSCOPED_INFO("CJK routed to SourceHanSans"); + REQUIRE(out[1].face == cjk.get()); + UNSCOPED_INFO("emoji routed to Noto-COLRv1"); + REQUIRE(out[2].face == emo.get()); // All three glyphs should have non-zero ids (no notdef anywhere). - ensure("'a' resolved to non-zero glyph", out[0].glyph_id != 0u); - ensure("CJK resolved to non-zero glyph", out[1].glyph_id != 0u); - ensure("emoji resolved to non-zero glyph", out[2].glyph_id != 0u); + UNSCOPED_INFO("'a' resolved to non-zero glyph"); + CHECK(out[0].glyph_id != 0u); + UNSCOPED_INFO("CJK resolved to non-zero glyph"); + CHECK(out[1].glyph_id != 0u); + UNSCOPED_INFO("emoji resolved to non-zero glyph"); + CHECK(out[2].glyph_id != 0u); } // VS-16 strip preserves cluster identity in the original wstr. @@ -375,14 +452,14 @@ namespace // emoji at position 3 stays cluster=3, even though it was at // position 2 in the post-strip buffer fed to HB). Pins // cluster_back_map rebase at alfontshaping.cpp:392-403. - template<> template<> - void alfontshaping_object::test<10>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: after a VS-16 strip clusters still index the original string", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFt(path); - ensure("Noto-COLRv1 loaded", ft.notNull()); + UNSCOPED_INFO("Noto-COLRv1 loaded"); + REQUIRE(ft.notNull()); // Heart + VS-16 + ZWJ + fire. The second codepoint (FE0F) gets // stripped. The output's clusters must still reference the original @@ -391,7 +468,8 @@ namespace const Text s = text(0x2764, 0xFE0F, 0x200D, 0x1F525); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure("output non-empty", !out.empty()); + UNSCOPED_INFO("output non-empty"); + REQUIRE_FALSE(out.empty()); // Whatever the final shape (ZWJ ligature collapses to 1 glyph, // or 2 with VS-16 stripped), every cluster must point into the // ORIGINAL string. A regression that left clusters in the post-strip @@ -400,10 +478,13 @@ namespace // the strip MUST rebase past it. for (const auto& g : out) { - ensure("cluster points into original string range", - g.cluster >= 0 && g.cluster < (S32)s.size()); - ensure_not_equals("cluster does NOT point at stripped VS-16 position", - g.cluster, (S32)s.at(1)); + { + INFO("cluster points into original string range"); + REQUIRE(g.cluster >= 0); + REQUIRE(g.cluster < (S32)s.size()); + } + UNSCOPED_INFO("cluster does NOT point at stripped VS-16 position"); + REQUIRE(g.cluster != (S32)s.at(1)); } } @@ -412,17 +493,20 @@ namespace // dropped by clearCacheForFace(A). The shape LRU is keyed on root // face only (alfontshaping.cpp:62-64); B's own entries (if any) // survive — we don't double-purge across heads sharing fallbacks. - template<> template<> - void alfontshaping_object::test<11>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: clearCacheForFace on a head keeps the entry of its fallback face", "[ALFontShaping]") { const std::string a_path = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string b_path = std::string(kFontDir) + "SourceHanSans-Regular.woff2"; if (!fileExists(a_path) || !fileExists(b_path)) - skip("DejaVuSans + SourceHanSans required"); + SKIP("DejaVuSans + SourceHanSans required"); LLPointer a = loadFt(a_path); LLPointer b = loadFt(b_path); - ensure("both loaded", a.notNull() && b.notNull()); + { + INFO("both loaded"); + REQUIRE(a.notNull()); + REQUIRE(b.notNull()); + } a->addFallbackFont(b); // Shape via A — its entry's glyph stream contains a B glyph @@ -430,8 +514,10 @@ namespace const Text s = text('a', 0x4F60); const auto& a_first = ALFontShaping::shapeLine(a, s, 0, s.size()); const auto& b_first = ALFontShaping::shapeLine(b, s, 0, s.size()); - ensure("a non-empty", !a_first.empty()); - ensure("b non-empty", !b_first.empty()); + UNSCOPED_INFO("a non-empty"); + REQUIRE_FALSE(a_first.empty()); + UNSCOPED_INFO("b non-empty"); + REQUIRE_FALSE(b_first.empty()); const void* a_addr = a_first.data(); const void* b_addr = b_first.data(); @@ -443,14 +529,15 @@ namespace // not load-bearing for the contract; B's survival is.) const auto& a_after = ALFontShaping::shapeLine(a, s, 0, s.size()); const auto& b_after = ALFontShaping::shapeLine(b, s, 0, s.size()); - ensure("A re-shape produces output", !a_after.empty()); - ensure_equals("B's entry survived clearCacheForFace(A)", - b_after.data(), b_addr); + UNSCOPED_INFO("A re-shape produces output"); + REQUIRE_FALSE(a_after.empty()); + UNSCOPED_INFO("B's entry survived clearCacheForFace(A)"); + REQUIRE(b_after.data() == b_addr); // Sanity: the data we read out of the survived B entry is the // same B stream we observed before — a hidden cross-purge would // have reset it. - ensure_equals("B output unchanged across A's cache flush", - b_after.size(), b_first.size()); + UNSCOPED_INFO("B output unchanged across A's cache flush"); + REQUIRE(b_after.size() == b_first.size()); } // notdef contract: shaping a codepoint through a head with NO @@ -458,75 +545,24 @@ namespace // glyph_id == 0). The renderer relies on this to draw the .notdef // rectangle for missing-glyph indication; an empty output would // silently drop the codepoint instead. - template<> template<> - void alfontshaping_object::test<12>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a codepoint no face covers shapes to one notdef glyph", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + UNSCOPED_INFO("DejaVuSans loaded"); + REQUIRE(ft.notNull()); // U+1F525 (fire emoji) is not in DejaVuSans's cmap, and we // attached no fallback, so HB must produce notdef. const Text s = text(0x1F525); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure_equals("uncovered codepoint produces 1 glyph", out.size(), 1u); - ensure_equals("uncovered codepoint glyph_id == 0 (notdef)", - out[0].glyph_id, 0u); - } - - // clearCache empties the LRU; subsequent shapes repopulate it. The - // earlier version of this test compared the returned reference's - // data() pointer before vs after clear and asserted the address - // changed — that was allocator-implementation-defined. Modern - // allocators (LFH on Windows, jemalloc, mimalloc, ...) recycle freed - // slots LIFO, so the rebuilt entry's vector storage routinely lands - // at the just-freed address and the assertion flaked ~3-of-5 runs. - // cacheSize() pins the contract the cache actually guarantees - // (count goes to 0 then back to 1) without depending on heap - // recycling behavior. - template<> template<> - void alfontshaping_object::test<8>() - { - const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; - if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); - LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); - - const Text s = text('h','i'); - const auto& first = ALFontShaping::shapeLine(ft, s, 0, s.size()); - ensure("shape produced output", !first.empty()); - ensure_equals("first shape leaves one entry", - ALFontShaping::cacheSize(), 1u); - // Snapshot the size BEFORE clearCache invalidates `first` — - // shapeLine returns a reference into the LRU, so the entry's - // storage is gone the moment the LRU is cleared. Reading - // first.size() afterward would be a use-after-free. - const size_t first_size = first.size(); - const void* addr0 = first.data(); - - // Cache hit: same storage AND the entry count stays at one. - const auto& second = ALFontShaping::shapeLine(ft, s, 0, s.size()); - ensure_equals("cache hit storage is stable", second.data(), addr0); - ensure_equals("cache hit doesn't add an entry", - ALFontShaping::cacheSize(), 1u); - - ALFontShaping::clearCache(); - ensure_equals("clearCache empties the LRU", - ALFontShaping::cacheSize(), 0u); - - // Miss: re-shape repopulates the cache. Output content is - // identical (shaping is deterministic given the same face + - // codepoints); the cache state is what changed. - const auto& after = ALFontShaping::shapeLine(ft, s, 0, s.size()); - ensure("post-clear rebuild produced output", !after.empty()); - ensure_equals("post-clear miss repopulated to one entry", - ALFontShaping::cacheSize(), 1u); - ensure_equals("post-clear shape is content-identical", - after.size(), first_size); + UNSCOPED_INFO("uncovered codepoint produces 1 glyph"); + REQUIRE(out.size() == 1u); + UNSCOPED_INFO("uncovered codepoint glyph_id == 0 (notdef)"); + CHECK(out[0].glyph_id == 0u); } // Keycap cluster across face boundary: '9' + U+FE0F + U+20E3 must @@ -536,23 +572,27 @@ namespace // fragment off onto Noto-COLRv1 — producing a bare '9' followed by a // standalone keycap mark with no base. Pins the keycap fix in // shape_all_sub_runs (alfontshaping.cpp:497-546). - template<> template<> - void alfontshaping_object::test<13>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: keycap 9 goes whole to the emoji fallback and composes to one glyph", "[ALFontShaping]") { const std::string head_path = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string emoji_path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(head_path) || !fileExists(emoji_path)) - skip("DejaVuSans + Noto-COLRv1 required"); + SKIP("DejaVuSans + Noto-COLRv1 required"); LLPointer head = loadFt(head_path); LLPointer emoji = loadFt(emoji_path); - ensure("both loaded", head.notNull() && emoji.notNull()); + { + INFO("both loaded"); + REQUIRE(head.notNull()); + REQUIRE(emoji.notNull()); + } head->addFallbackFont(emoji); const Text s = text('9', 0xFE0F, 0x20E3); std::vector out; ALFontShaping::shapeRun(head, s, 0, s.size(), out); - ensure("keycap shaped to non-empty output", !out.empty()); + UNSCOPED_INFO("keycap shaped to non-empty output"); + REQUIRE_FALSE(out.empty()); // The emoji face has the GSUB rule digit + U+20E3 -> keycap glyph // (VS-16 stripped pre-shape). All output glyphs must come from // the emoji face — not head — to confirm the cluster routed as @@ -560,16 +600,16 @@ namespace // '9' glyph on `head` and only the keycap mark on `emoji`. for (const auto& g : out) { - ensure_equals("every keycap-cluster glyph routes to emoji face", - g.face, emoji.get()); + UNSCOPED_INFO("every keycap-cluster glyph routes to emoji face"); + REQUIRE(g.face == emoji.get()); } // Strong pin: the GSUB rule actually fires across the face // boundary. A weaker form of the regression — routing matches // but GSUB doesn't collapse — would leave 2+ glyphs on the // emoji face (the '9' outline plus an unbound keycap mark), // matching exactly what the user sees in the buggy report. - ensure_equals("keycap '9️⃣' GSUB-collapsed to a single glyph", - out.size(), 1u); + UNSCOPED_INFO("keycap '9️⃣' GSUB-collapsed to a single glyph"); + REQUIRE(out.size() == 1u); } // ZWJ heart-on-fire across face boundary: U+2764 (BMP heart, present @@ -577,42 +617,113 @@ namespace // emoji face). Phase 1 routes the whole cluster to the emoji face so // its GSUB can collapse the ZWJ ligature. A regression to per-cp // itemization would split the heart onto head, breaking the rule. - template<> template<> - void alfontshaping_object::test<14>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: the ZWJ heart-on-fire goes whole to the emoji fallback and ligates", "[ALFontShaping]") { const std::string head_path = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string emoji_path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(head_path) || !fileExists(emoji_path)) - skip("DejaVuSans + Noto-COLRv1 required"); + SKIP("DejaVuSans + Noto-COLRv1 required"); LLPointer head = loadFt(head_path); LLPointer emoji = loadFt(emoji_path); - ensure("both loaded", head.notNull() && emoji.notNull()); + { + INFO("both loaded"); + REQUIRE(head.notNull()); + REQUIRE(emoji.notNull()); + } head->addFallbackFont(emoji); // Sanity: head DOES carry U+2764 — selectShapingFace would // happily route it to head without the cluster fast path. - ensure("head face has U+2764 in cmap (without Phase 1 the heart " - "would route to head and split the cluster)", - head->getCharGlyphIndex(0x2764) != 0u); + UNSCOPED_INFO("head face has U+2764 in cmap (without Phase 1 the heart " + "would route to head and split the cluster)"); + CHECK(head->getCharGlyphIndex(0x2764) != 0u); const Text s = text(0x2764, 0xFE0F, 0x200D, 0x1F525); std::vector out; ALFontShaping::shapeRun(head, s, 0, s.size(), out); - ensure("heart-on-fire shaped to non-empty output", !out.empty()); + UNSCOPED_INFO("heart-on-fire shaped to non-empty output"); + REQUIRE_FALSE(out.empty()); for (const auto& g : out) { - ensure_equals("every heart-on-fire glyph routes to emoji face", - g.face, emoji.get()); + UNSCOPED_INFO("every heart-on-fire glyph routes to emoji face"); + REQUIRE(g.face == emoji.get()); } // Pin the ligature too — Noto-COLRv1's GSUB collapses the // sequence to a single glyph. Test 6 covers the same rule when // shaping directly through Noto; this asserts it still fires when // the head is a non-emoji font and the cluster is routed across // the fallback boundary. - ensure_equals("heart-on-fire ZWJ ligated to a single glyph " - "even across the fallback boundary", - out.size(), 1u); + UNSCOPED_INFO("heart-on-fire ZWJ ligated to a single glyph " + "even across the fallback boundary"); + REQUIRE(out.size() == 1u); + } + + // Bare digit control: '9' alone (no FE0F + 20E3 follower) is NOT a + // cluster per the walker. Phase 1's fast path must not interfere — + // the digit routes to head as before, one glyph, no surprises. + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a bare 9 is no emoji cluster and stays on the head face", "[ALFontShaping]") + { + const std::string head_path = std::string(kFontDir) + "DejaVuSans.woff2"; + const std::string emoji_path = std::string(kFontDir) + "Noto-COLRv1.ttf"; + if (!fileExists(head_path) || !fileExists(emoji_path)) + SKIP("DejaVuSans + Noto-COLRv1 required"); + + LLPointer head = loadFt(head_path); + LLPointer emoji = loadFt(emoji_path); + { + INFO("both loaded"); + REQUIRE(head.notNull()); + REQUIRE(emoji.notNull()); + } + head->addFallbackFont(emoji); + + const Text s = text('9'); + std::vector out; + ALFontShaping::shapeRun(head, s, 0, s.size(), out); + UNSCOPED_INFO("bare '9' produces one glyph"); + REQUIRE(out.size() == 1u); + UNSCOPED_INFO("bare '9' routes to head (not emoji fallback)"); + REQUIRE(out[0].face == head.get()); + UNSCOPED_INFO("bare '9' glyph_id matches head's cmap"); + CHECK(out[0].glyph_id == head->getCharGlyphIndex(L'9')); + } + + // Hash-keycap directly through the emoji face: pin that Noto-COLRv1 + // actually has the GSUB rule for '#' + VS-16 + U+20E3 -> composed + // keycap (one glyph). The '9' rule is exercised by test 13 across + // a face boundary; this version uses Noto-COLRv1 directly so a font- + // level missing rule is observable as glyph_count > 1 (not a routing + // bug). User report: '#️⃣' renders as '#' + a standalone keycap mark + // — diagnosed by this test as either a font-rule miss or a routing + // miss, with the existing rule-fires path through 9️⃣ as the control. + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: keycap # composes to one glyph through Noto-COLRv1 alone", "[ALFontShaping]") + { + const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; + if (!fileExists(path)) + SKIP("Noto-COLRv1.ttf not present"); + LLPointer ft = loadFt(path); + UNSCOPED_INFO("Noto-COLRv1 loaded"); + REQUIRE(ft.notNull()); + + // Sanity: the font carries '#' and U+20E3 in cmap. If either + // were missing, hb_shape would emit notdef before any GSUB + // could match. + UNSCOPED_INFO("Noto-COLRv1 has '#' in cmap"); + CHECK(ft->getCharGlyphIndex(L'#') != 0u); + UNSCOPED_INFO("Noto-COLRv1 has U+20E3 in cmap"); + CHECK(ft->getCharGlyphIndex(0x20E3) != 0u); + + const Text s = text('#', 0xFE0F, 0x20E3); + std::vector out; + ALFontShaping::shapeRun(ft, s, 0, s.size(), out); + UNSCOPED_INFO("hash-keycap shaped to non-empty output"); + REQUIRE_FALSE(out.empty()); + // If the GSUB keycap rule fires, the run collapses to a single + // composed glyph (the same shape test 6 / test 13 expect for + // their respective ligatures). + UNSCOPED_INFO("hash-keycap '#️⃣' GSUB-collapsed to a single glyph"); + REQUIRE(out.size() == 1u); } // Adjacent clusters: '3️⃣' (keycap) + '🏳️‍🌈' (pride flag, ZWJ @@ -623,17 +734,20 @@ namespace // down to a bare white flag (the rainbow part of the ZWJ ligature // collapsed against the wrong context). Each cluster must shape as // its own sub-run, so each rule sees only its own codepoints. - template<> template<> - void alfontshaping_object::test<17>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: adjacent keycap and pride-flag clusters each shape on their own", "[ALFontShaping]") { const std::string head_path = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string emoji_path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(head_path) || !fileExists(emoji_path)) - skip("DejaVuSans + Noto-COLRv1 required"); + SKIP("DejaVuSans + Noto-COLRv1 required"); LLPointer head = loadFt(head_path); LLPointer emoji = loadFt(emoji_path); - ensure("both loaded", head.notNull() && emoji.notNull()); + { + INFO("both loaded"); + REQUIRE(head.notNull()); + REQUIRE(emoji.notNull()); + } head->addFallbackFont(emoji); // '3' VS-16 U+20E3 (keycap), then U+1F3F3 (waving white flag) @@ -642,19 +756,20 @@ namespace 0x1F3F3, 0xFE0F, 0x200D, 0x1F308); std::vector out; ALFontShaping::shapeRun(head, s, 0, s.size(), out); - ensure("adjacent clusters shaped to non-empty output", !out.empty()); + UNSCOPED_INFO("adjacent clusters shaped to non-empty output"); + REQUIRE_FALSE(out.empty()); for (const auto& g : out) { - ensure_equals("every glyph routes to emoji face", - g.face, emoji.get()); + UNSCOPED_INFO("every glyph routes to emoji face"); + REQUIRE(g.face == emoji.get()); } // Both clusters compose: keycap '3️⃣' -> 1 glyph, pride flag // '🏳️‍🌈' -> 1 glyph. Total 2. A regression that merged the // two clusters into one buffer would produce a different count // (e.g. the pride flag's GSUB matching against the keycap mark // and falling back to bare white-flag + standalone rainbow). - ensure_equals("each adjacent cluster collapses independently", - out.size(), 2u); + UNSCOPED_INFO("each adjacent cluster collapses independently"); + REQUIRE(out.size() == 2u); } // Cluster with no emoji-face coverage: route to root rather than to @@ -665,31 +780,37 @@ namespace // We model this by attaching DejaVu (which has '#' but lacks U+20E3) // as the only fallback and shaping a keycap. Result must keep '#' // visible on root rather than tofu-ing the base on a non-covering face. - template<> template<> - void alfontshaping_object::test<18>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a keycap no face can compose keeps its base # on the root face", "[ALFontShaping]") { const std::string head_path = std::string(kFontDir) + "InterVariable.woff2"; const std::string fallback_path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(head_path) || !fileExists(fallback_path)) - skip("InterVariable + DejaVuSans required"); + SKIP("InterVariable + DejaVuSans required"); LLPointer head = loadFt(head_path); LLPointer fallback = loadFt(fallback_path); - ensure("both loaded", head.notNull() && fallback.notNull()); + { + INFO("both loaded"); + REQUIRE(head.notNull()); + REQUIRE(fallback.notNull()); + } head->addFallbackFont(fallback); // Sanity: neither face has U+20E3 (keycap mark) — that's the // scenario this test exercises. Neither has the keycap GSUB // rule. Best-case outcome: the base '#' renders through root. - ensure_equals("Inter lacks U+20E3", head->getCharGlyphIndex(0x20E3), 0u); - ensure_equals("DejaVu lacks U+20E3", fallback->getCharGlyphIndex(0x20E3), 0u); - ensure_not_equals("Inter has '#' in cmap", - head->getCharGlyphIndex(L'#'), 0u); + UNSCOPED_INFO("Inter lacks U+20E3"); + CHECK(head->getCharGlyphIndex(0x20E3) == 0u); + UNSCOPED_INFO("DejaVu lacks U+20E3"); + CHECK(fallback->getCharGlyphIndex(0x20E3) == 0u); + UNSCOPED_INFO("Inter has '#' in cmap"); + CHECK(head->getCharGlyphIndex(L'#') != 0u); const Text s = text('#', 0xFE0F, 0x20E3); std::vector out; ALFontShaping::shapeRun(head, s, 0, s.size(), out); - ensure("shape produced output", !out.empty()); + UNSCOPED_INFO("shape produced output"); + REQUIRE_FALSE(out.empty()); // The base '#' must end up on a face that has it (root). A // regression that routed to a non-covering face would tofu // the '#' (glyph_id == 0) which is the worse failure mode. @@ -699,70 +820,8 @@ namespace if (g.face == head.get() && g.glyph_id == head->getCharGlyphIndex(L'#')) found_hash = true; } - ensure("'#' base routed to a face that has it", - found_hash); - } - - // Hash-keycap directly through the emoji face: pin that Noto-COLRv1 - // actually has the GSUB rule for '#' + VS-16 + U+20E3 -> composed - // keycap (one glyph). The '9' rule is exercised by test 13 across - // a face boundary; this version uses Noto-COLRv1 directly so a font- - // level missing rule is observable as glyph_count > 1 (not a routing - // bug). User report: '#️⃣' renders as '#' + a standalone keycap mark - // — diagnosed by this test as either a font-rule miss or a routing - // miss, with the existing rule-fires path through 9️⃣ as the control. - template<> template<> - void alfontshaping_object::test<16>() - { - const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; - if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); - LLPointer ft = loadFt(path); - ensure("Noto-COLRv1 loaded", ft.notNull()); - - // Sanity: the font carries '#' and U+20E3 in cmap. If either - // were missing, hb_shape would emit notdef before any GSUB - // could match. - ensure_not_equals("Noto-COLRv1 has '#' in cmap", - ft->getCharGlyphIndex(L'#'), 0u); - ensure_not_equals("Noto-COLRv1 has U+20E3 in cmap", - ft->getCharGlyphIndex(0x20E3), 0u); - - const Text s = text('#', 0xFE0F, 0x20E3); - std::vector out; - ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure("hash-keycap shaped to non-empty output", !out.empty()); - // If the GSUB keycap rule fires, the run collapses to a single - // composed glyph (the same shape test 6 / test 13 expect for - // their respective ligatures). - ensure_equals("hash-keycap '#️⃣' GSUB-collapsed to a single glyph", - out.size(), 1u); - } - - // Bare digit control: '9' alone (no FE0F + 20E3 follower) is NOT a - // cluster per the walker. Phase 1's fast path must not interfere — - // the digit routes to head as before, one glyph, no surprises. - template<> template<> - void alfontshaping_object::test<15>() - { - const std::string head_path = std::string(kFontDir) + "DejaVuSans.woff2"; - const std::string emoji_path = std::string(kFontDir) + "Noto-COLRv1.ttf"; - if (!fileExists(head_path) || !fileExists(emoji_path)) - skip("DejaVuSans + Noto-COLRv1 required"); - - LLPointer head = loadFt(head_path); - LLPointer emoji = loadFt(emoji_path); - ensure("both loaded", head.notNull() && emoji.notNull()); - head->addFallbackFont(emoji); - - const Text s = text('9'); - std::vector out; - ALFontShaping::shapeRun(head, s, 0, s.size(), out); - ensure_equals("bare '9' produces one glyph", out.size(), 1u); - ensure_equals("bare '9' routes to head (not emoji fallback)", - out[0].face, head.get()); - ensure_equals("bare '9' glyph_id matches head's cmap", - out[0].glyph_id, head->getCharGlyphIndex(L'9')); + UNSCOPED_INFO("'#' base routed to a face that has it"); + CHECK(found_hash); } // Subdivision flag: U+1F3F4 (black flag) + tag chars 'gbeng' + the @@ -772,17 +831,20 @@ namespace // to a face whose cmap covers the tag chars. A regression that // dropped tag-char support from the predicate would split the flag // off from its tag bytes and render the bytes as visible glyphs. - template<> template<> - void alfontshaping_object::test<19>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a tag-sequence subdivision flag goes whole to the emoji fallback", "[ALFontShaping]") { const std::string head_path = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string emoji_path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(head_path) || !fileExists(emoji_path)) - skip("DejaVuSans + Noto-COLRv1 required"); + SKIP("DejaVuSans + Noto-COLRv1 required"); LLPointer head = loadFt(head_path); LLPointer emoji = loadFt(emoji_path); - ensure("both loaded", head.notNull() && emoji.notNull()); + { + INFO("both loaded"); + REQUIRE(head.notNull()); + REQUIRE(emoji.notNull()); + } head->addFallbackFont(emoji); // 🏴 (U+1F3F4) + 'g' 'b' 'e' 'n' 'g' tag chars + U+E007F. @@ -791,23 +853,24 @@ namespace 0xE007F); std::vector out; ALFontShaping::shapeRun(head, s, 0, s.size(), out); - ensure("subdivision flag shaped to non-empty output", !out.empty()); + UNSCOPED_INFO("subdivision flag shaped to non-empty output"); + REQUIRE_FALSE(out.empty()); // Routing assertion is the load-bearing one: every cluster // member must land on the emoji face. A predicate regression // that excluded tag chars would split the flag onto emoji and // the tag bytes onto root. for (const auto& g : out) { - ensure_equals("every tag-flag glyph routes to emoji face", - g.face, emoji.get()); + UNSCOPED_INFO("every tag-flag glyph routes to emoji face"); + REQUIRE(g.face == emoji.get()); } // Stretch goal: if Noto-COLRv1 carries the GB-ENG subdivision // flag, GSUB collapses the whole sequence to one glyph. Some // Noto-COLRv1 builds ship without subdivision flags — tolerate // up to N glyphs, but never MORE than the input length (which // would mean glyph duplication). - ensure("output glyph count never exceeds input codepoint count", - out.size() <= s.size()); + UNSCOPED_INFO("output glyph count never exceeds input codepoint count"); + REQUIRE(out.size() <= s.size()); } // Cell-alignment probe. For each monospace face shipped in @@ -820,8 +883,7 @@ namespace // long lines. If it ever does, the strict feature plan in // shape_sub_run needs more entries (and the long-form fix is a // cluster-aware advance snap on HB output). - template<> template<> - void alfontshaping_object::test<20>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: monospace HB shaping keeps 200 glyphs on the cell with no drift", "[ALFontShaping]") { const char* mono_files[] = { "DejaVuSansMono.woff2", @@ -851,23 +913,27 @@ namespace if (!fileExists(path)) continue; LLPointer ft = loadFt(path); - ensure("monospace font loaded", ft.notNull()); - ensure("font is fixed-width", ft->isFixedWidth()); + UNSCOPED_INFO("monospace font loaded"); + REQUIRE(ft.notNull()); + UNSCOPED_INFO("font is fixed-width"); + REQUIRE(ft->isFixedWidth()); ft->setAllowMonospaceLigatures(true); // route through HB std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure_equals("N codepoints produce N glyphs through HB", - out.size(), N); + UNSCOPED_INFO("N codepoints produce N glyphs through HB"); + REQUIRE(out.size() == N); // Reference: first glyph's advance. In a fixed-width face // all ASCII codepoints share one FT mXAdvance, so out[0] // sets the canonical cell. Deliberately don't use // ft->getXAdvance — that path asserts !mIsFallback and // the probe loads as fallback to skip GL pre-warm. - ensure("at least one glyph", !out.empty()); + UNSCOPED_INFO("at least one glyph"); + REQUIRE_FALSE(out.empty()); const F32 ref_advance = out[0].x_advance; - ensure("reference advance positive", ref_advance > 0.f); + UNSCOPED_INFO("reference advance positive"); + REQUIRE(ref_advance > 0.f); F32 max_dev = 0.f; F32 min_adv = ref_advance; @@ -901,17 +967,21 @@ namespace // Tolerance: 0.5px is generous — meaningful column drift // shows as ~1px by char ~50 in production reports. - ensure("per-glyph advance drift < 0.5px", max_dev < 0.5f); - ensure("cumulative advance drift < 0.5px", cumulative_dev < 0.5f); + UNSCOPED_INFO("per-glyph advance drift < 0.5px"); + REQUIRE(max_dev < 0.5f); + UNSCOPED_INFO("cumulative advance drift < 0.5px"); + REQUIRE(cumulative_dev < 0.5f); // Bypass writes 0 offsets; HB output should match for // ASCII (no GPOS positioning adjustments fire on monospace // ASCII with kern off). - ensure("HB ASCII x_offset is zero", max_xoff < 0.5f); - ensure("HB ASCII y_offset is zero", max_yoff < 0.5f); + UNSCOPED_INFO("HB ASCII x_offset is zero"); + REQUIRE(max_xoff < 0.5f); + UNSCOPED_INFO("HB ASCII y_offset is zero"); + REQUIRE(max_yoff < 0.5f); ++fonts_tested; } if (fonts_tested == 0) - skip("no monospace fonts present in test data dir"); + SKIP("no monospace fonts present in test data dir"); } // Strict-mono never collapses codepoints into ligated glyphs. @@ -929,8 +999,7 @@ namespace // reasons: (1) catches a future shipped-font upgrade that DOES // include programmer ligatures, and (2) catches an HB upstream // change that fires more lookups by default. - template<> template<> - void alfontshaping_object::test<21>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: strict monospace never ligates: one glyph per codepoint", "[ALFontShaping]") { const char* mono_files[] = { "DejaVuSansMono.woff2", @@ -951,23 +1020,25 @@ namespace if (!fileExists(path)) continue; LLPointer ft = loadFt(path); - ensure("monospace font loaded", ft.notNull()); - ensure("font is fixed-width", ft->isFixedWidth()); + UNSCOPED_INFO("monospace font loaded"); + REQUIRE(ft.notNull()); + UNSCOPED_INFO("font is fixed-width"); + REQUIRE(ft->isFixedWidth()); // Default: ligatures off. Routes through HB with kFixedWidthStrict. - ensure("ligatures default off", - !ft->getAllowMonospaceLigatures()); + UNSCOPED_INFO("ligatures default off"); + REQUIRE_FALSE(ft->getAllowMonospaceLigatures()); // All ASCII, so a byte is a codepoint here too. const std::string& s = ligature_bait; std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure_equals("strict-mono glyph_count == codepoint_count", - out.size(), s.size()); + UNSCOPED_INFO("strict-mono glyph_count == codepoint_count"); + REQUIRE(out.size() == s.size()); ++fonts_tested; } if (fonts_tested == 0) - skip("no monospace fonts present in test data dir"); + SKIP("no monospace fonts present in test data dir"); } // Combining marks position via GPOS in monospace shaping. The retired @@ -981,15 +1052,16 @@ namespace // cluster index, the second has zero x_advance (mark consumes no // cell), and the mark has non-zero positioning (offset OR cluster // composition through ccmp into a precomposed 'á'). - template<> template<> - void alfontshaping_object::test<22>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a combining acute on monospace composes or sits as a zero-advance mark", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSansMono.woff2"; if (!fileExists(path)) - skip("DejaVuSansMono.woff2 not present"); + SKIP("DejaVuSansMono.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSansMono loaded", ft.notNull()); - ensure("DejaVuSansMono is fixed-width", ft->isFixedWidth()); + UNSCOPED_INFO("DejaVuSansMono loaded"); + REQUIRE(ft.notNull()); + UNSCOPED_INFO("DejaVuSansMono is fixed-width"); + CHECK(ft->isFixedWidth()); const Text s = text(L'a', 0x0301); // a + COMBINING ACUTE ACCENT std::vector out; @@ -1003,7 +1075,8 @@ namespace k, out[k].glyph_id, out[k].cluster, out[k].x_advance, out[k].x_offset, out[k].y_offset); } - ensure("combining cluster produces at least 1 glyph", !out.empty()); + UNSCOPED_INFO("combining cluster produces at least 1 glyph"); + REQUIRE_FALSE(out.empty()); // Expected outcomes: // (a) ccmp composed to precomposed 'á' → 1 glyph, full cell advance. // (b) Two glyphs (a + acute) in one HB cluster → first carries @@ -1012,23 +1085,28 @@ namespace // Either is correct GPOS-driven behavior; the bypass produced // neither (it emitted 2 glyphs, both with full advance). const F32 ref_advance = out[0].x_advance; - ensure("base glyph has positive advance", ref_advance > 0.f); + UNSCOPED_INFO("base glyph has positive advance"); + CHECK(ref_advance > 0.f); if (out.size() == 1) { // ccmp composition path. One cell, no second glyph. - ensure_equals("cluster index is 0 (base)", out[0].cluster, 0); + UNSCOPED_INFO("cluster index is 0 (base)"); + CHECK(out[0].cluster == 0); } else if (out.size() == 2) { // Mark cluster path. Marks share the base's cluster index // and consume zero advance (a hard requirement — a non-zero // mark advance would shift the next character right). - ensure_equals("mark shares base cluster", out[1].cluster, out[0].cluster); - ensure_equals("mark glyph has zero advance", out[1].x_advance, 0.f); + UNSCOPED_INFO("mark shares base cluster"); + CHECK(out[1].cluster == out[0].cluster); + UNSCOPED_INFO("mark glyph has zero advance"); + CHECK(out[1].x_advance == 0.f); } else { - ensure("combining cluster produced unexpected glyph count", false); + UNSCOPED_INFO("combining cluster produced unexpected glyph count"); + CHECK(false); } } @@ -1060,19 +1138,21 @@ namespace // hhea, ignoring USE_TYPO_METRICS), while FT's size->metrics // ascender honors USE_TYPO_METRICS — so they differ by design. // hb_ft_font_create_referenced leaves ppem at 0 (only scale is set). - template<> template<> - void alfontshaping_object::test<23>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: HB's font scale is what hb-ft derives from the FT face's size", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + UNSCOPED_INFO("DejaVuSans loaded"); + REQUIRE(ft.notNull()); const ALFontFace* face = ft->getFontFace(); - ensure("face accessible", face != nullptr); + UNSCOPED_INFO("face accessible"); + REQUIRE(face != nullptr); hb_font_t* hbf = face->getHbFont(); - ensure("hb_font accessible", hbf != nullptr); + UNSCOPED_INFO("hb_font accessible"); + REQUIRE(hbf != nullptr); int hb_x_scale = 0, hb_y_scale = 0; hb_font_get_scale(hbf, &hb_x_scale, &hb_y_scale); @@ -1083,13 +1163,15 @@ namespace // avoid 32-bit overflow at large UPEM × scale products. const int expected_x = (int)((((std::uint64_t)m.x_scale * (std::uint64_t)ftf->units_per_EM) + (1u << 15)) >> 16); const int expected_y = (int)((((std::uint64_t)m.y_scale * (std::uint64_t)ftf->units_per_EM) + (1u << 15)) >> 16); - ensure_equals("HB x_scale matches hb-ft derivation from FT state", - hb_x_scale, expected_x); - ensure_equals("HB y_scale matches hb-ft derivation from FT state", - hb_y_scale, expected_y); + UNSCOPED_INFO("HB x_scale matches hb-ft derivation from FT state"); + CHECK(hb_x_scale == expected_x); + UNSCOPED_INFO("HB y_scale matches hb-ft derivation from FT state"); + CHECK(hb_y_scale == expected_y); // Sanity: scale must be positive (zero would mean uninitialized). - ensure("HB x_scale is positive", hb_x_scale > 0); - ensure("HB y_scale is positive", hb_y_scale > 0); + UNSCOPED_INFO("HB x_scale is positive"); + CHECK(hb_x_scale > 0); + UNSCOPED_INFO("HB y_scale is positive"); + CHECK(hb_y_scale > 0); } // (test<24> intentionally absent — formerly checked hb_font_get_ppem @@ -1104,14 +1186,14 @@ namespace // outline scaling or load-flag plumbing. DejaVuSans has no GPOS // kern between adjacent lowercase letters, so HB's GPOS pass is // a no-op and per-glyph advances come straight from FT. - template<> template<> - void alfontshaping_object::test<25>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: HB advances equal FreeType's for an unkerned ASCII run", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + UNSCOPED_INFO("DejaVuSans loaded"); + REQUIRE(ft.notNull()); const ALFontFace* face = ft->getFontFace(); FT_Face ftf = face->face(); @@ -1120,19 +1202,20 @@ namespace const Text s = text('a','b','c','d','e','f'); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure_equals("6 glyphs for 'abcdef'", out.size(), 6u); + UNSCOPED_INFO("6 glyphs for 'abcdef'"); + REQUIRE(out.size() == 6u); for (size_t i = 0; i < out.size(); ++i) { - ensure_equals("FT_Load_Glyph succeeded", - FT_Load_Glyph(ftf, out[i].glyph_id, load_flags), 0); + UNSCOPED_INFO("FT_Load_Glyph succeeded"); + REQUIRE(FT_Load_Glyph(ftf, out[i].glyph_id, load_flags) == 0); const F32 ft_advance = ftf->glyph->advance.x * (1.f / 64.f); // 26.6 -> float; equality in float at this precision // would be exact when no GPOS adjustment fired. Tolerate // 1/64 px to cover HB's internal rounding paths. const F32 delta = std::fabs(out[i].x_advance - ft_advance); - ensure("HB x_advance matches FT advance for unkerned glyph", - delta < (1.f / 64.f) + 1e-5f); + UNSCOPED_INFO("HB x_advance matches FT advance for unkerned glyph"); + REQUIRE(delta < (1.f / 64.f) + 1e-5f); } } @@ -1141,27 +1224,28 @@ namespace // refactor that splits the casts asymmetrically breaks shaped vs // codepoint advance consistency. hb_ft_font_get_load_flags has // existed since HB 1.7 — the codebase requires newer than that. - template<> template<> - void alfontshaping_object::test<26>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: HB's load flags are the hinting FT renders glyphs with", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + UNSCOPED_INFO("DejaVuSans loaded"); + REQUIRE(ft.notNull()); const ALFontFace* face = ft->getFontFace(); hb_font_t* hbf = face->getHbFont(); - ensure("hb_font accessible", hbf != nullptr); + UNSCOPED_INFO("hb_font accessible"); + REQUIRE(hbf != nullptr); const int hb_flags = hb_ft_font_get_load_flags(hbf); const int expected = static_cast(face->hinting()); - ensure_equals("HB load flags == (int)mHinting", - hb_flags, expected); + UNSCOPED_INFO("HB load flags == (int)mHinting"); + CHECK(hb_flags == expected); // And confirm the cast equivalence between the two surfaces // (HB takes int, FT takes FT_Int32) hasn't drifted. - ensure_equals("(int)mHinting == (FT_Int32)mHinting", - static_cast(face->hinting()), + UNSCOPED_INFO("(int)mHinting == (FT_Int32)mHinting"); + CHECK(static_cast(face->hinting()) == (int)static_cast(face->hinting())); } @@ -1170,12 +1254,11 @@ namespace // font's default axis values (typically wght=400) even when FT // renders varied outlines, breaking variation-aware kerning on // fonts like Inter at non-default weights. - template<> template<> - void alfontshaping_object::test<27>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: HB shapes at the variation design coordinates FT renders at", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(path)) - skip("InterVariable.woff2 not present"); + SKIP("InterVariable.woff2 not present"); // Load with wght=600 so setVariationAxis sets a non-default // wght. opsz is auto-set from point_size whenever opsz_set is // false (load() handles both axes via the matching *_set @@ -1183,8 +1266,8 @@ namespace ALFontVarAxes va; va.wght = 600.f; va.wght_set = true; LLPointer ft = new LLFontFreetype; - ensure("InterVariable loaded at weight=600", - ft->loadFace(path, /*point_size=*/14.f, + UNSCOPED_INFO("InterVariable loaded at weight=600"); + CHECK(ft->loadFace(path, /*point_size=*/14.f, /*vert_dpi=*/96.f, /*horz_dpi=*/96.f, /*is_fallback=*/true, /*face_n=*/0, EFontHinting::DEFAULT, @@ -1193,23 +1276,28 @@ namespace const ALFontFace* face = ft->getFontFace(); FT_Face ftf = face->face(); hb_font_t* hbf = face->getHbFont(); - ensure("hb_font accessible", hbf != nullptr); + UNSCOPED_INFO("hb_font accessible"); + REQUIRE(hbf != nullptr); FT_MM_Var* mm = nullptr; - ensure_equals("FT_Get_MM_Var on a variable font", - FT_Get_MM_Var(ftf, &mm), 0); - ensure("FT_MM_Var allocated", mm != nullptr); + UNSCOPED_INFO("FT_Get_MM_Var on a variable font"); + CHECK(FT_Get_MM_Var(ftf, &mm) == 0); + UNSCOPED_INFO("FT_MM_Var allocated"); + REQUIRE(mm != nullptr); const FT_UInt num_axis = mm->num_axis; - ensure("variable font reports >= 1 axis", num_axis >= 1u); + UNSCOPED_INFO("variable font reports >= 1 axis"); + CHECK(num_axis >= 1u); std::vector ft_coords(num_axis); - ensure_equals("FT_Get_Var_Design_Coordinates", - FT_Get_Var_Design_Coordinates(ftf, num_axis, ft_coords.data()), 0); + UNSCOPED_INFO("FT_Get_Var_Design_Coordinates"); + CHECK(FT_Get_Var_Design_Coordinates(ftf, num_axis, ft_coords.data()) == 0); unsigned int hb_len = 0; const float* hb_coords = hb_font_get_var_coords_design(hbf, &hb_len); - ensure("HB has design coords set", hb_coords != nullptr); - ensure_equals("HB axis count matches FT", hb_len, (unsigned int)num_axis); + UNSCOPED_INFO("HB has design coords set"); + REQUIRE(hb_coords != nullptr); + UNSCOPED_INFO("HB axis count matches FT"); + CHECK(hb_len == (unsigned int)num_axis); for (FT_UInt i = 0; i < num_axis; ++i) { @@ -1217,8 +1305,8 @@ namespace const float delta = std::fabs(hb_coords[i] - ft_design); // 16.16 round-trip: equality should be exact, but allow // 1/65536 to cover any internal float-conversion rounding. - ensure("HB design coord matches FT design coord", - delta < (1.f / 65536.f) + 1e-6f); + UNSCOPED_INFO("HB design coord matches FT design coord"); + REQUIRE(delta < (1.f / 65536.f) + 1e-6f); } FT_Done_MM_Var(gFTLibrary, mm); @@ -1230,10 +1318,10 @@ namespace // 2.7+, but cff/type1 default ON; without explicit Property_Set on // those, a CFF font with EFontHinting::DEFAULT picks up unwanted // stem darkening. - template<> template<> - void alfontshaping_object::test<28>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: stem darkening is off on every FreeType module that has the property", "[ALFontShaping]") { - ensure("gFTLibrary initialized", gFTLibrary != nullptr); + UNSCOPED_INFO("gFTLibrary initialized"); + REQUIRE(gFTLibrary != nullptr); const char* modules[] = { "autofitter", "cff", "type1", "t1cid" }; for (const char* mod : modules) @@ -1247,8 +1335,8 @@ namespace // a no-op. if (err != 0) continue; - ensure(std::string("no-stem-darkening enabled on ") + mod, - no_darken != 0); + UNSCOPED_INFO(std::string("no-stem-darkening enabled on ") + mod); + REQUIRE(no_darken != 0); } } @@ -1256,20 +1344,21 @@ namespace // Unicode font — the resulting active charmap encoding should be // FT_ENCODING_UNICODE so all FT_Get_Char_Index lookups operate on // the Unicode cmap subtable, not whichever subtable FT auto-picked. - template<> template<> - void alfontshaping_object::test<29>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a Unicode font's active FreeType charmap is the Unicode one", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + UNSCOPED_INFO("DejaVuSans loaded"); + REQUIRE(ft.notNull()); const ALFontFace* face = ft->getFontFace(); FT_Face ftf = face->face(); - ensure("active charmap is set", ftf->charmap != nullptr); - ensure_equals("active charmap encoding is FT_ENCODING_UNICODE", - (int)ftf->charmap->encoding, + UNSCOPED_INFO("active charmap is set"); + REQUIRE(ftf->charmap != nullptr); + UNSCOPED_INFO("active charmap encoding is FT_ENCODING_UNICODE"); + CHECK((int)ftf->charmap->encoding == (int)FT_ENCODING_UNICODE); } @@ -1278,19 +1367,19 @@ namespace // vs just `` — the do_shape strip pass should drop VS-15 // before HB so the notdef glyph for VS-15 doesn't sit between // base and follow-up codepoints. Mirrors test<7>'s VS-16 proof. - template<> template<> - void alfontshaping_object::test<30>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a VS-15 the cmap lacks is stripped as VS-16 is", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFt(path); - ensure("Noto-COLRv1 loaded", ft.notNull()); + UNSCOPED_INFO("Noto-COLRv1 loaded"); + REQUIRE(ft.notNull()); // Sanity: the test only verifies anything if Noto-COLRv1 also // lacks VS-15. Color emoji fonts typically don't ship VS-15. if (ft->getCharGlyphIndex(0xFE0E) != 0u) - skip("Noto-COLRv1 unexpectedly has U+FE0E in cmap"); + SKIP("Noto-COLRv1 unexpectedly has U+FE0E in cmap"); const Text with_vs = text(0x2764, 0xFE0E); const Text without_vs = text(0x2764); @@ -1300,13 +1389,14 @@ namespace ALFontShaping::shapeRun(ft, with_vs, 0, with_vs.size(), with_out); ALFontShaping::shapeRun(ft, without_vs, 0, without_vs.size(), without_out); - ensure_equals("with-VS15 and without-VS15 produce same glyph count", - with_out.size(), without_out.size()); - ensure("with-VS15 produced at least one glyph", !with_out.empty()); + UNSCOPED_INFO("with-VS15 and without-VS15 produce same glyph count"); + REQUIRE(with_out.size() == without_out.size()); + UNSCOPED_INFO("with-VS15 produced at least one glyph"); + REQUIRE_FALSE(with_out.empty()); for (size_t i = 0; i < with_out.size(); ++i) { - ensure_equals("with-VS15 glyph_id matches without-VS15", - with_out[i].glyph_id, without_out[i].glyph_id); + UNSCOPED_INFO("with-VS15 glyph_id matches without-VS15"); + REQUIRE(with_out[i].glyph_id == without_out[i].glyph_id); } } @@ -1315,14 +1405,14 @@ namespace // ASCII; this extends the contract to the upper BMP so a regression // limited to Unicode-cmap binding (Defect B in the audit plan) is // caught even when ASCII still works. - template<> template<> - void alfontshaping_object::test<31>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a non-ASCII codepoint shapes to the glyph the cmap gives it", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + UNSCOPED_INFO("DejaVuSans loaded"); + REQUIRE(ft.notNull()); // Pick the first non-ASCII codepoint the face has. DejaVuSans // covers Greek and Cyrillic; try a Greek alpha (U+03B1) first. @@ -1337,16 +1427,17 @@ namespace } } if (wch == 0) - skip("no non-ASCII codepoint covered in DejaVuSans"); + SKIP("no non-ASCII codepoint covered in DejaVuSans"); const Text s = text(wch); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure_equals("1 glyph for non-ASCII codepoint", out.size(), 1u); - ensure_equals("shape glyph_id matches cmap lookup", - out[0].glyph_id, ft->getCharGlyphIndex(wch)); - ensure("non-ASCII glyph has positive advance", - out[0].x_advance > 0.f); + UNSCOPED_INFO("1 glyph for non-ASCII codepoint"); + REQUIRE(out.size() == 1u); + UNSCOPED_INFO("shape glyph_id matches cmap lookup"); + CHECK(out[0].glyph_id == ft->getCharGlyphIndex(wch)); + UNSCOPED_INFO("non-ASCII glyph has positive advance"); + CHECK(out[0].x_advance > 0.f); } // Cluster index invariant: every glyph emitted by shapeRun has @@ -1355,14 +1446,14 @@ namespace // that index per-cluster advances by cluster. ZWJ-retry candidates // and corrupt GSUB tables can synthesize cluster values outside the // input range; the clamp pins them back into the slice. - template<> template<> - void alfontshaping_object::test<32>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: every cluster shapeRun emits lies within the slice it shaped", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + UNSCOPED_INFO("DejaVuSans loaded"); + REQUIRE(ft.notNull()); // begin/end are written as codepoint indices and converted per case. struct Case { Text s; size_t begin; size_t end; }; @@ -1385,10 +1476,10 @@ namespace ALFontShaping::shapeRun(ft, c.s, begin_bytes, end_bytes, out); for (const auto& g : out) { - ensure(("cluster >= begin (case " + std::to_string(ci) + ")").c_str(), - g.cluster >= (S32)begin_bytes); - ensure(("cluster < end (case " + std::to_string(ci) + ")").c_str(), - g.cluster < (S32)end_bytes); + UNSCOPED_INFO(("cluster >= begin (case " + std::to_string(ci) + ")").c_str()); + REQUIRE(g.cluster >= (S32)begin_bytes); + UNSCOPED_INFO(("cluster < end (case " + std::to_string(ci) + ")").c_str()); + REQUIRE(g.cluster < (S32)end_bytes); } } } @@ -1405,14 +1496,14 @@ namespace // fallback) — DejaVu has ZWJ but lacks the astral pictographs and // the GSUB rule for the composed flag, so HB emits one glyph per // covered codepoint plus notdefs. - template<> template<> - void alfontshaping_object::test<33>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: every glyph of an unligated emoji cluster carries the cluster's start", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + UNSCOPED_INFO("DejaVuSans loaded"); + REQUIRE(ft.notNull()); // "X" + trans flag (5 cps, [1, 6)) + "Y". The cluster walker // identifies [1, 6) as one emoji cluster; the fast path shapes it @@ -1423,7 +1514,8 @@ namespace 0xFE0F, L'Y'); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure("shape produced output", !out.empty()); + UNSCOPED_INFO("shape produced output"); + REQUIRE_FALSE(out.empty()); // Every glyph whose cluster falls inside the flag must report the // cluster's start. Glyphs for X and Y are unaffected. @@ -1434,12 +1526,12 @@ namespace { if (g.cluster >= flag_begin && g.cluster < flag_end) saw_cluster_glyph = true; - ensure("trans-flag cluster glyphs collapse to cluster start", - !(g.cluster > flag_begin && g.cluster < flag_end)); + UNSCOPED_INFO("trans-flag cluster glyphs collapse to cluster start"); + REQUIRE_FALSE((g.cluster > flag_begin && g.cluster < flag_end)); } - ensure("at least one glyph routed through the cluster sub-run " - "(otherwise this test isn't exercising the fast path)", - saw_cluster_glyph); + UNSCOPED_INFO("at least one glyph routed through the cluster sub-run " + "(otherwise this test isn't exercising the fast path)"); + CHECK(saw_cluster_glyph); } // addFallbackFont must invalidate cached shape entries for the head: @@ -1452,46 +1544,52 @@ namespace // ALFontShaping cache dirty — so the second shape call returned the // stale entry and rendered tofu through A's notdef even though the // CJK fallback was attached and would have covered the codepoint. - template<> template<> - void alfontshaping_object::test<34>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: addFallbackFont drops cached shapes so the new fallback is used", "[ALFontShaping]") { const std::string a_path = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string b_path = std::string(kFontDir) + "SourceHanSans-Regular.woff2"; if (!fileExists(a_path) || !fileExists(b_path)) - skip("DejaVuSans + SourceHanSans required"); + SKIP("DejaVuSans + SourceHanSans required"); LLPointer a = loadFt(a_path); LLPointer b = loadFt(b_path); - ensure("both loaded", a.notNull() && b.notNull()); + { + INFO("both loaded"); + REQUIRE(a.notNull()); + REQUIRE(b.notNull()); + } // Pre-fallback shape: U+4F60 (你) is not in DejaVuSans's cmap, so // HB returns one notdef glyph (glyph_id=0) on face A. const Text s = text(0x4F60); std::vector pre; ALFontShaping::shapeRun(a, s, 0, s.size(), pre); - ensure_equals("pre-fallback shape produces 1 glyph", pre.size(), 1u); - ensure_equals("pre-fallback glyph routes to head A", pre[0].face, a.get()); - ensure_equals("pre-fallback glyph is notdef (glyph_id=0)", - pre[0].glyph_id, 0u); - ensure_equals("pre-fallback shape leaves one cache entry", - ALFontShaping::cacheSize(), 1u); + UNSCOPED_INFO("pre-fallback shape produces 1 glyph"); + REQUIRE(pre.size() == 1u); + UNSCOPED_INFO("pre-fallback glyph routes to head A"); + REQUIRE(pre[0].face == a.get()); + UNSCOPED_INFO("pre-fallback glyph is notdef (glyph_id=0)"); + CHECK(pre[0].glyph_id == 0u); + UNSCOPED_INFO("pre-fallback shape leaves one cache entry"); + CHECK(ALFontShaping::cacheSize() == 1u); // Attach the CJK fallback. addFallbackFont must clear the cached // entry for `a` so the next shape() re-itemizes through the new // chain instead of returning the stale notdef. a->addFallbackFont(b); - ensure_equals("addFallbackFont(this) drops cached entries for this", - ALFontShaping::cacheSize(), 0u); + UNSCOPED_INFO("addFallbackFont(this) drops cached entries for this"); + CHECK(ALFontShaping::cacheSize() == 0u); // Post-fallback shape: U+4F60 should now route to face B with a // non-zero glyph_id (B has the CJK glyph in its cmap). std::vector post; ALFontShaping::shapeRun(a, s, 0, s.size(), post); - ensure_equals("post-fallback shape produces 1 glyph", post.size(), 1u); - ensure_equals("post-fallback glyph routes to fallback B", - post[0].face, b.get()); - ensure_not_equals("post-fallback glyph is real (not notdef)", - post[0].glyph_id, 0u); + UNSCOPED_INFO("post-fallback shape produces 1 glyph"); + REQUIRE(post.size() == 1u); + UNSCOPED_INFO("post-fallback glyph routes to fallback B"); + REQUIRE(post[0].face == b.get()); + UNSCOPED_INFO("post-fallback glyph is real (not notdef)"); + CHECK(post[0].glyph_id != 0u); } // Two Unicode tables live in this process: HarfBuzz's own, which its @@ -1504,8 +1602,7 @@ namespace // They agree today, across the whole range. This holds them to it: bumping // either package past the other's Unicode version should fail here rather // than quietly change how text is shaped. - template<> template<> - void alfontshaping_object::test<35>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: HarfBuzz's and ICU's Unicode tables agree on every codepoint", "[ALFontShaping]") { auto icu_to_hb = [](int8_t t) -> hb_unicode_general_category_t { @@ -1565,11 +1662,11 @@ namespace } } - ensure_equals("HarfBuzz and ICU agree on general category" - " (first disagreement at U+" + llformat("%04X", first_category) + ")", - category_diffs, 0); - ensure_equals("HarfBuzz and ICU agree on combining class" - " (first disagreement at U+" + llformat("%04X", first_combining) + ")", - combining_diffs, 0); + UNSCOPED_INFO("HarfBuzz and ICU agree on general category" + " (first disagreement at U+" + llformat("%04X", first_category) + ")"); + CHECK(category_diffs == 0); + UNSCOPED_INFO("HarfBuzz and ICU agree on combining class" + " (first disagreement at U+" + llformat("%04X", first_combining) + ")"); + CHECK(combining_diffs == 0); } } diff --git a/indra/llrender/tests/algpubuffer_gl_test.cpp b/indra/llrender/tests/algpubuffer_gl_test.cpp index ec140236a6..f4f8fa9819 100644 --- a/indra/llrender/tests/algpubuffer_gl_test.cpp +++ b/indra/llrender/tests/algpubuffer_gl_test.cpp @@ -35,17 +35,15 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include #include #include #include -namespace tut +namespace { - namespace - { constexpr U32 WORDS = 64; // A point at the centre, from no vertex arrays. @@ -108,7 +106,11 @@ namespace tut { const GLuint vs = ll_test::compileTestShader(GL_VERTEX_SHADER, kVertex); const GLuint fs = ll_test::compileTestShader(GL_FRAGMENT_SHADER, kFragment); - ensure("shaders compile", vs && fs); + { + INFO("shaders compile"); + REQUIRE(vs); + REQUIRE(fs); + } mProgram.mProgramObject = glCreateProgram(); mProgram.mName = "indirect point (test)"; mProgram.attachObject(vs); @@ -116,8 +118,10 @@ namespace tut const bool linked = mProgram.mapAttributes(); glDeleteShader(vs); glDeleteShader(fs); - ensure("program links", linked); - ensure("program maps", mProgram.mapUniforms()); + UNSCOPED_INFO("program links"); + REQUIRE(linked); + UNSCOPED_INFO("program maps"); + REQUIRE(mProgram.mapUniforms()); mProgram.bind(); } @@ -126,119 +130,155 @@ namespace tut LLGLSLShader mProgram; }; - typedef test_group algpubuffer_t; - typedef algpubuffer_t::object algpubuffer_object_t; - tut::algpubuffer_t tut_algpubuffer("algpubuffer_gl"); - } // What a buffer allocated with data holds, what an upload, a clear of a range and a whole clear leave, and what // the store accounts for while it lives. - template<> template<> - void algpubuffer_object_t::test<1>() + TEST_CASE_METHOD(algpubuffer_data, "algpubuffer_gl: a DYNAMIC buffer holds what is written and accounts its store", "[algpubuffer_gl]") { const U64 before = ALGPUBuffer::sBytesAllocated; std::vector expected = pattern(WORDS, 1000u); { ALGPUBuffer buffer; - ensure("allocates", buffer.allocate(WORDS * sizeof(U32), ALGPUBuffer::DYNAMIC, expected.data(), "words (test)")); - ensure("allocated", buffer.isAllocated()); - ensure_equals("its size", buffer.size(), size_t(WORDS * sizeof(U32))); - ensure_equals("its flags", buffer.getFlags(), U32(ALGPUBuffer::DYNAMIC)); - ensure_equals("accounted", ALGPUBuffer::sBytesAllocated, before + WORDS * sizeof(U32)); - ensure("holds its data", readAll(buffer) == expected); + UNSCOPED_INFO("allocates"); + REQUIRE(buffer.allocate(WORDS * sizeof(U32), ALGPUBuffer::DYNAMIC, expected.data(), "words (test)")); + UNSCOPED_INFO("allocated"); + CHECK(buffer.isAllocated()); + UNSCOPED_INFO("its size"); + REQUIRE(buffer.size() == size_t(WORDS * sizeof(U32))); + UNSCOPED_INFO("its flags"); + CHECK(buffer.getFlags() == U32(ALGPUBuffer::DYNAMIC)); + UNSCOPED_INFO("accounted"); + CHECK(ALGPUBuffer::sBytesAllocated == before + WORDS * sizeof(U32)); + UNSCOPED_INFO("holds its data"); + CHECK(readAll(buffer) == expected); const std::vector middle = pattern(8, 77u); buffer.upload(16 * sizeof(U32), middle.data(), middle.size() * sizeof(U32)); std::copy(middle.begin(), middle.end(), expected.begin() + 16); - ensure("holds the upload where it went", readAll(buffer) == expected); + UNSCOPED_INFO("holds the upload where it went"); + CHECK(readAll(buffer) == expected); buffer.clearRange(40 * sizeof(U32), 6 * sizeof(U32), 0xDEADBEEFu); std::fill(expected.begin() + 40, expected.begin() + 46, 0xDEADBEEFu); - ensure("holds the cleared range, and only it", readAll(buffer) == expected); + UNSCOPED_INFO("holds the cleared range, and only it"); + CHECK(readAll(buffer) == expected); std::vector part(4, 0u); - ensure("reads at an offset", buffer.read(42 * sizeof(U32), part.data(), part.size() * sizeof(U32))); - ensure("the words there", part == std::vector(expected.begin() + 42, expected.begin() + 46)); + UNSCOPED_INFO("reads at an offset"); + REQUIRE(buffer.read(42 * sizeof(U32), part.data(), part.size() * sizeof(U32))); + UNSCOPED_INFO("the words there"); + CHECK(part == std::vector(expected.begin() + 42, expected.begin() + 46)); buffer.clear(7u); - ensure("whole clear", readAll(buffer) == std::vector(WORDS, 7u)); + UNSCOPED_INFO("whole clear"); + CHECK(readAll(buffer) == std::vector(WORDS, 7u)); } - ensure_equals("destruction returns what it accounted", ALGPUBuffer::sBytesAllocated, before); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("destruction returns what it accounted"); + CHECK(ALGPUBuffer::sBytesAllocated == before); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // Copies between buffers that are not DYNAMIC, and within one, go where they are told; and such a buffer clears // where the clear is the GPU's. Below GL 4.3 the clear is an upload, which a static store does not take, and is // asserted: the buffers a clear is for are compute's, and compute needs 4.3. - template<> template<> - void algpubuffer_object_t::test<2>() + TEST_CASE_METHOD(algpubuffer_data, "algpubuffer_gl: copies between and within static buffers land where they are told", "[algpubuffer_gl]") { const std::vector source_words = pattern(WORDS, 5u); std::vector expected(WORDS, 0u); ALGPUBuffer source; ALGPUBuffer target; - ensure("source allocates", source.allocate(WORDS * sizeof(U32), 0, source_words.data(), "source (test)")); - ensure("target allocates", target.allocate(WORDS * sizeof(U32), 0, expected.data(), "target (test)")); + UNSCOPED_INFO("source allocates"); + REQUIRE(source.allocate(WORDS * sizeof(U32), 0, source_words.data(), "source (test)")); + UNSCOPED_INFO("target allocates"); + REQUIRE(target.allocate(WORDS * sizeof(U32), 0, expected.data(), "target (test)")); target.copyFrom(source, 4 * sizeof(U32), 32 * sizeof(U32), 10 * sizeof(U32)); std::copy(source_words.begin() + 4, source_words.begin() + 14, expected.begin() + 32); - ensure("copied between buffers", readAll(target) == expected); + UNSCOPED_INFO("copied between buffers"); + CHECK(readAll(target) == expected); target.copyFrom(target, 32 * sizeof(U32), 0, 10 * sizeof(U32)); std::copy(expected.begin() + 32, expected.begin() + 42, expected.begin()); - ensure("copied within one", readAll(target) == expected); - ensure("the source untouched", readAll(source) == source_words); + UNSCOPED_INFO("copied within one"); + CHECK(readAll(target) == expected); + UNSCOPED_INFO("the source untouched"); + CHECK(readAll(source) == source_words); if (clearsOnGPU()) { target.clear(0xA5A5A5A5u); - ensure("a static buffer clears", readAll(target) == std::vector(WORDS, 0xA5A5A5A5u)); + UNSCOPED_INFO("a static buffer clears"); + CHECK(readAll(target) == std::vector(WORDS, 0xA5A5A5A5u)); } - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // release empties a buffer and returns its accounting; a move takes the store and leaves the source empty, and a // move over a buffer that held a store releases that store. - template<> template<> - void algpubuffer_object_t::test<3>() + TEST_CASE_METHOD(algpubuffer_data, "algpubuffer_gl: release empties a buffer and a move takes its store", "[algpubuffer_gl]") { const U64 before = ALGPUBuffer::sBytesAllocated; const std::vector words = pattern(WORDS, 9u); ALGPUBuffer first; - ensure("allocates", first.allocate(WORDS * sizeof(U32), 0, words.data(), "first (test)")); + UNSCOPED_INFO("allocates"); + REQUIRE(first.allocate(WORDS * sizeof(U32), 0, words.data(), "first (test)")); first.release(); - ensure("released", !first.isAllocated()); - ensure_equals("no size", first.size(), size_t(0)); - ensure_equals("no name", first.getGLName(), 0u); - ensure_equals("accounting returned", ALGPUBuffer::sBytesAllocated, before); - - ensure("allocates again", first.allocate(WORDS * sizeof(U32), 0, words.data(), "first (test)")); + UNSCOPED_INFO("released"); + CHECK_FALSE(first.isAllocated()); + UNSCOPED_INFO("no size"); + REQUIRE(first.size() == size_t(0)); + UNSCOPED_INFO("no name"); + CHECK(first.getGLName() == 0u); + UNSCOPED_INFO("accounting returned"); + CHECK(ALGPUBuffer::sBytesAllocated == before); + + UNSCOPED_INFO("allocates again"); + CHECK(first.allocate(WORDS * sizeof(U32), 0, words.data(), "first (test)")); const U32 name = first.getGLName(); ALGPUBuffer moved(std::move(first)); - ensure("the source is empty", !first.isAllocated() && first.size() == 0 && first.getGLName() == 0); - ensure_equals("the target took the name", moved.getGLName(), name); - ensure("and the store", readAll(moved) == words); - ensure_equals("accounted once", ALGPUBuffer::sBytesAllocated, before + WORDS * sizeof(U32)); + { + INFO("the source is empty"); + CHECK_FALSE(first.isAllocated()); + REQUIRE(first.size() == 0); + CHECK(first.getGLName() == 0); + } + UNSCOPED_INFO("the target took the name"); + CHECK(moved.getGLName() == name); + UNSCOPED_INFO("and the store"); + CHECK(readAll(moved) == words); + UNSCOPED_INFO("accounted once"); + CHECK(ALGPUBuffer::sBytesAllocated == before + WORDS * sizeof(U32)); ALGPUBuffer other; - ensure("other allocates", other.allocate(2 * WORDS * sizeof(U32), 0, nullptr, "other (test)")); + UNSCOPED_INFO("other allocates"); + REQUIRE(other.allocate(2 * WORDS * sizeof(U32), 0, nullptr, "other (test)")); other = std::move(moved); - ensure("the moved-from is empty", !moved.isAllocated() && moved.getGLName() == 0); - ensure_equals("the assigned took the name", other.getGLName(), name); - ensure("and the store", readAll(other) == words); - ensure_equals("its own store released", ALGPUBuffer::sBytesAllocated, before + WORDS * sizeof(U32)); + { + INFO("the moved-from is empty"); + CHECK_FALSE(moved.isAllocated()); + CHECK(moved.getGLName() == 0); + } + UNSCOPED_INFO("the assigned took the name"); + CHECK(other.getGLName() == name); + UNSCOPED_INFO("and the store"); + CHECK(readAll(other) == words); + UNSCOPED_INFO("its own store released"); + CHECK(ALGPUBuffer::sBytesAllocated == before + WORDS * sizeof(U32)); other.release(); - ensure_equals("all returned", ALGPUBuffer::sBytesAllocated, before); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("all returned"); + CHECK(ALGPUBuffer::sBytesAllocated == before); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // LLRender binds an indirect draw's buffer once and keeps it bound; after refreshState it binds it again, since // what is bound is no longer known; and a buffer's release forgets its name, so a new buffer that GL hands the // same name is bound rather than taken for bound already. - template<> template<> - void algpubuffer_object_t::test<4>() + TEST_CASE_METHOD(algpubuffer_data, "algpubuffer_gl: LLRender keeps an indirect draw's buffer bound and rebinds when it must", "[algpubuffer_gl]") { setUpProgram(); ALDrawArraysIndirectCommand command; @@ -246,111 +286,141 @@ namespace tut command.mInstanceCount = 1; ALGPUBuffer first; - ensure("allocates", first.allocate(sizeof(command), 0, &command, "draw (test)")); + UNSCOPED_INFO("allocates"); + REQUIRE(first.allocate(sizeof(command), 0, &command, "draw (test)")); gGL.drawArraysIndirect(LLRender::POINTS, first, 0); - ensure_equals("bound for the draw", indirectBinding(), (GLint)first.getGLName()); + UNSCOPED_INFO("bound for the draw"); + CHECK(indirectBinding() == (GLint)first.getGLName()); gGL.drawArraysIndirect(LLRender::POINTS, first, 0); - ensure_equals("still bound", indirectBinding(), (GLint)first.getGLName()); + UNSCOPED_INFO("still bound"); + CHECK(indirectBinding() == (GLint)first.getGLName()); // Behind LLRender's back. glBindBuffer(GL_DRAW_INDIRECT_BUFFER, 0); gGL.refreshState(); gGL.drawArraysIndirect(LLRender::POINTS, first, 0); - ensure_equals("bound again after refreshState", indirectBinding(), (GLint)first.getGLName()); - ensure_equals("no GL error after refreshState", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("bound again after refreshState"); + CHECK(indirectBinding() == (GLint)first.getGLName()); + UNSCOPED_INFO("no GL error after refreshState"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); const U32 name = first.getGLName(); first.release(); - ensure_equals("deleting it unbound it", indirectBinding(), 0); + UNSCOPED_INFO("deleting it unbound it"); + CHECK(indirectBinding() == 0); ALGPUBuffer second; - ensure("another allocates", second.allocate(sizeof(command), 0, &command, "draw again (test)")); + UNSCOPED_INFO("another allocates"); + REQUIRE(second.allocate(sizeof(command), 0, &command, "draw again (test)")); if (second.getGLName() != name) { - skip("GL did not hand the released name out again"); + SKIP("GL did not hand the released name out again"); } gGL.drawArraysIndirect(LLRender::POINTS, second, 0); - ensure_equals("the reused name is bound", indirectBinding(), (GLint)second.getGLName()); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("the reused name is bound"); + CHECK(indirectBinding() == (GLint)second.getGLName()); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A READBACK store is mapped where persistent maps resolve, and a STREAM one too; an upload into a mapped STREAM // store goes through the map, and the GPU sees it in a copy issued after it. - template<> template<> - void algpubuffer_object_t::test<5>() + TEST_CASE_METHOD(algpubuffer_data, "algpubuffer_gl: READBACK and STREAM stores are mapped where persistent maps resolve", "[algpubuffer_gl]") { const bool persistent = glBufferStorage != nullptr; ALGPUBuffer readback; ALGPUBuffer stream; ALGPUBuffer check; - ensure("readback allocates", readback.allocate(WORDS * sizeof(U32), ALGPUBuffer::READBACK, nullptr, "readback (test)")); - ensure("stream allocates", stream.allocate(WORDS * sizeof(U32), ALGPUBuffer::STREAM, nullptr, "stream (test)")); - ensure("check allocates", check.allocate(WORDS * sizeof(U32), 0, nullptr, "check (test)")); - ensure_equals("readback mapped where persistent maps resolve", readback.getMapped() != nullptr, persistent); - ensure_equals("stream too", stream.getMapped() != nullptr, persistent); + UNSCOPED_INFO("readback allocates"); + REQUIRE(readback.allocate(WORDS * sizeof(U32), ALGPUBuffer::READBACK, nullptr, "readback (test)")); + UNSCOPED_INFO("stream allocates"); + REQUIRE(stream.allocate(WORDS * sizeof(U32), ALGPUBuffer::STREAM, nullptr, "stream (test)")); + UNSCOPED_INFO("check allocates"); + REQUIRE(check.allocate(WORDS * sizeof(U32), 0, nullptr, "check (test)")); + UNSCOPED_INFO("readback mapped where persistent maps resolve"); + REQUIRE((readback.getMapped() != nullptr) == persistent); + UNSCOPED_INFO("stream too"); + REQUIRE((stream.getMapped() != nullptr) == persistent); const std::vector words = pattern(WORDS, 300u); stream.upload(0, words.data(), words.size() * sizeof(U32)); check.copyFrom(stream, 0, 0, WORDS * sizeof(U32)); - ensure("the GPU sees the stream's upload", readAll(check) == words); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("the GPU sees the stream's upload"); + CHECK(readAll(check) == words); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // ALGPUReadback: each request copies what its source held then; a request with every slot pending is refused; the // results come back oldest first; and with none pending collect has nothing to give. - template<> template<> - void algpubuffer_object_t::test<6>() + TEST_CASE_METHOD(algpubuffer_data, "algpubuffer_gl: ALGPUReadback gives back each request's copy oldest first", "[algpubuffer_gl]") { constexpr U32 SLOTS = 3; constexpr U32 COUNT = 16; ALGPUBuffer source; - ensure("source allocates", source.allocate(WORDS * sizeof(U32), ALGPUBuffer::DYNAMIC, nullptr, "source (test)")); + UNSCOPED_INFO("source allocates"); + REQUIRE(source.allocate(WORDS * sizeof(U32), ALGPUBuffer::DYNAMIC, nullptr, "source (test)")); ALGPUReadback readback; - ensure("readback allocates", readback.allocate(COUNT * sizeof(U32), SLOTS, "readback (test)")); - ensure_equals("a slot's size", readback.getSlotSize(), size_t(COUNT * sizeof(U32))); + UNSCOPED_INFO("readback allocates"); + REQUIRE(readback.allocate(COUNT * sizeof(U32), SLOTS, "readback (test)")); + UNSCOPED_INFO("a slot's size"); + CHECK(readback.getSlotSize() == size_t(COUNT * sizeof(U32))); std::vector result(COUNT, 0u); - ensure("nothing to collect before a request", !readback.collect(result.data(), result.size() * sizeof(U32))); + UNSCOPED_INFO("nothing to collect before a request"); + REQUIRE_FALSE(readback.collect(result.data(), result.size() * sizeof(U32))); for (U32 i = 0; i < SLOTS; ++i) { const std::vector words = pattern(WORDS, 1000u * (i + 1)); source.upload(0, words.data(), words.size() * sizeof(U32)); - ensure("request " + std::to_string(i), readback.request(source, 8 * sizeof(U32), COUNT * sizeof(U32))); + UNSCOPED_INFO("request " + std::to_string(i)); + REQUIRE(readback.request(source, 8 * sizeof(U32), COUNT * sizeof(U32))); } - ensure_equals("every slot pending", readback.getPending(), SLOTS); - ensure("a request past them refused", !readback.request(source, 0, COUNT * sizeof(U32))); + UNSCOPED_INFO("every slot pending"); + CHECK(readback.getPending() == SLOTS); + UNSCOPED_INFO("a request past them refused"); + CHECK_FALSE(readback.request(source, 0, COUNT * sizeof(U32))); for (U32 i = 0; i < SLOTS; ++i) { - ensure("collect " + std::to_string(i), readback.collect(result.data(), result.size() * sizeof(U32), true)); + UNSCOPED_INFO("collect " + std::to_string(i)); + REQUIRE(readback.collect(result.data(), result.size() * sizeof(U32), true)); const std::vector words = pattern(WORDS, 1000u * (i + 1)); - ensure("result " + std::to_string(i) + " is what the source held at its request", - result == std::vector(words.begin() + 8, words.begin() + 8 + COUNT)); + UNSCOPED_INFO("result " + std::to_string(i) + " is what the source held at its request"); + REQUIRE(result == std::vector(words.begin() + 8, words.begin() + 8 + COUNT)); } - ensure_equals("none pending", readback.getPending(), 0u); - ensure("nothing more to collect", !readback.collect(result.data(), result.size() * sizeof(U32), true)); + UNSCOPED_INFO("none pending"); + CHECK(readback.getPending() == 0u); + UNSCOPED_INFO("nothing more to collect"); + REQUIRE_FALSE(readback.collect(result.data(), result.size() * sizeof(U32), true)); // A request made, its copy finished: collect gives it without waiting. const std::vector last = pattern(WORDS, 7u); source.upload(0, last.data(), last.size() * sizeof(U32)); - ensure("one more request", readback.request(source, 0, COUNT * sizeof(U32))); + UNSCOPED_INFO("one more request"); + CHECK(readback.request(source, 0, COUNT * sizeof(U32))); glFinish(); - ensure("a finished copy is collected without waiting", readback.collect(result.data(), result.size() * sizeof(U32))); - ensure("and holds the source", result == std::vector(last.begin(), last.begin() + COUNT)); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("a finished copy is collected without waiting"); + REQUIRE(readback.collect(result.data(), result.size() * sizeof(U32))); + UNSCOPED_INFO("and holds the source"); + CHECK(result == std::vector(last.begin(), last.begin() + COUNT)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // ALGPUStream: each map gives the next region, round the ring, at offsets a storage range may start at; what is // written there and published is what the GPU reads at the offset publish returns; and the ring wraps. - template<> template<> - void algpubuffer_object_t::test<7>() + TEST_CASE_METHOD(algpubuffer_data, "algpubuffer_gl: ALGPUStream maps each region in turn round the ring", "[algpubuffer_gl]") { constexpr U32 REGIONS = 3; constexpr U32 COUNT = 10; ALGPUStream stream; - ensure("stream allocates", stream.allocate(COUNT * sizeof(U32), REGIONS, "stream (test)")); - ensure_equals("a region's size", stream.getRegionSize(), size_t(COUNT * sizeof(U32))); + UNSCOPED_INFO("stream allocates"); + REQUIRE(stream.allocate(COUNT * sizeof(U32), REGIONS, "stream (test)")); + UNSCOPED_INFO("a region's size"); + CHECK(stream.getRegionSize() == size_t(COUNT * sizeof(U32))); ALGPUBuffer check; - ensure("check allocates", check.allocate(COUNT * sizeof(U32), 0, nullptr, "check (test)")); + UNSCOPED_INFO("check allocates"); + REQUIRE(check.allocate(COUNT * sizeof(U32), 0, nullptr, "check (test)")); const size_t alignment = gGLManager.mShaderStorageBufferOffsetAlignment > 0 ? (size_t)gGLManager.mShaderStorageBufferOffsetAlignment @@ -359,24 +429,30 @@ namespace tut for (U32 frame = 0; frame < 2 * REGIONS; ++frame) { U8* region = stream.map(); - ensure("a region to write", region != nullptr); + UNSCOPED_INFO("a region to write"); + REQUIRE(region != nullptr); const std::vector words = pattern(COUNT, 100u * (frame + 1)); memcpy(region, words.data(), words.size() * sizeof(U32)); const size_t offset = stream.publish(words.size() * sizeof(U32)); - ensure("frame " + std::to_string(frame) + " at an aligned offset", offset % alignment == 0); + UNSCOPED_INFO("frame " + std::to_string(frame) + " at an aligned offset"); + REQUIRE(offset % alignment == 0); offsets.push_back(offset); check.copyFrom(stream.getBuffer(), offset, 0, COUNT * sizeof(U32)); - ensure("frame " + std::to_string(frame) + " reaches the GPU", readAll(check) == words); + UNSCOPED_INFO("frame " + std::to_string(frame) + " reaches the GPU"); + REQUIRE(readAll(check) == words); } for (U32 frame = 0; frame < REGIONS; ++frame) { for (U32 other = frame + 1; other < REGIONS; ++other) { - ensure("the ring's regions apart", offsets[frame] != offsets[other]); + UNSCOPED_INFO("the ring's regions apart"); + REQUIRE(offsets[frame] != offsets[other]); } - ensure_equals("the ring wraps to the same region", offsets[frame + REGIONS], offsets[frame]); + UNSCOPED_INFO("the ring wraps to the same region"); + REQUIRE(offsets[frame + REGIONS] == offsets[frame]); } - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } } diff --git a/indra/llrender/tests/alimageslot_gl_test.cpp b/indra/llrender/tests/alimageslot_gl_test.cpp index f51ef9b00b..196d5aa962 100644 --- a/indra/llrender/tests/alimageslot_gl_test.cpp +++ b/indra/llrender/tests/alimageslot_gl_test.cpp @@ -38,16 +38,14 @@ #include "llimage.h" -#include "../test/lltut.h" +#include #include #include #include -namespace tut +namespace { - namespace - { constexpr U32 SIZE = 8; // What GL holds at image unit `unit`. @@ -125,12 +123,13 @@ namespace tut { if (!ll_test::contextHasCompute()) { - skip("the context has no compute"); + SKIP("the context has no compute"); } LLRender::sCompute = true; - ensure("target allocates", - mTarget.allocate(SIZE, SIZE, GL_R32F, false, false, ALTextureSlot::TT_TEXTURE, LLRenderTarget::MIPS_MANUAL)); - ensure("other target allocates", mOther.allocate(SIZE, SIZE, GL_RGBA16F)); + UNSCOPED_INFO("target allocates"); + REQUIRE(mTarget.allocate(SIZE, SIZE, GL_R32F, false, false, ALTextureSlot::TT_TEXTURE, LLRenderTarget::MIPS_MANUAL)); + UNSCOPED_INFO("other target allocates"); + REQUIRE(mOther.allocate(SIZE, SIZE, GL_RGBA16F)); } ll_test::SharedGLScope mGL; @@ -140,106 +139,142 @@ namespace tut GLuint mKernel = 0; }; - typedef test_group alimageslot_t; - typedef alimageslot_t::object alimageslot_object_t; - tut::alimageslot_t tut_alimageslot("alimageslot_gl"); - } // A bind reaches GL as stated, a bind of what the unit already holds issues nothing, a change of any part of it // issues one call, and unbind returns the unit to texture 0 once. - template<> template<> - void alimageslot_object_t::test<1>() + TEST_CASE_METHOD(alimageslot_data, "alimageslot_gl: a bind reaches GL as stated and only a change issues a call", "[alimageslot_gl]") { setUp(); ALImageSlot* slot = gGL.getImageSlot(2); - ensure_equals("the slot is its unit", slot->getIndex(), 2); + UNSCOPED_INFO("the slot is its unit"); + CHECK(slot->getIndex() == 2); U32 binds = ALImageSlot::sImageBinds; - ensure("bound", slot->bind(&mTarget, 0, ALImageSlot::READ_WRITE, 1)); - ensure_equals("one call", ALImageSlot::sImageBinds, binds + 1); + UNSCOPED_INFO("bound"); + CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_WRITE, 1)); + UNSCOPED_INFO("one call"); + CHECK(ALImageSlot::sImageBinds == binds + 1); UnitState state = unitState(2); - ensure_equals("GL holds the texture", state.mTexture, (GLint)mTarget.getTexture()); - ensure_equals("at its level", state.mLevel, 1); - ensure_equals("not layered", state.mLayered, GL_FALSE); - ensure_equals("read-write", state.mAccess, GL_READ_WRITE); - ensure_equals("at the target's own format", state.mFormat, GL_R32F); - ensure_equals("the slot says so", slot->getCurrTexture(), mTarget.getTexture()); + UNSCOPED_INFO("GL holds the texture"); + CHECK(state.mTexture == (GLint)mTarget.getTexture()); + UNSCOPED_INFO("at its level"); + CHECK(state.mLevel == 1); + UNSCOPED_INFO("not layered"); + CHECK(state.mLayered == GL_FALSE); + UNSCOPED_INFO("read-write"); + CHECK(state.mAccess == GL_READ_WRITE); + UNSCOPED_INFO("at the target's own format"); + CHECK(state.mFormat == GL_R32F); + UNSCOPED_INFO("the slot says so"); + CHECK(slot->getCurrTexture() == mTarget.getTexture()); binds = ALImageSlot::sImageBinds; - ensure("bound again", slot->bind(&mTarget, 0, ALImageSlot::READ_WRITE, 1)); - ensure_equals("the same bind issues nothing", ALImageSlot::sImageBinds, binds); - - ensure("another access", slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY, 1)); - ensure_equals("a new access issues one call", ALImageSlot::sImageBinds, binds + 1); - ensure_equals("GL holds the new access", unitState(2).mAccess, GL_READ_ONLY); - - ensure("another level", slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY, 0)); - ensure_equals("a new level issues one call", ALImageSlot::sImageBinds, binds + 2); - ensure_equals("GL holds the new level", unitState(2).mLevel, 0); - - ensure("another texture", slot->bind(&mOther, 0, ALImageSlot::READ_ONLY, 0)); - ensure_equals("a new texture issues one call", ALImageSlot::sImageBinds, binds + 3); + UNSCOPED_INFO("bound again"); + CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_WRITE, 1)); + UNSCOPED_INFO("the same bind issues nothing"); + CHECK(ALImageSlot::sImageBinds == binds); + + UNSCOPED_INFO("another access"); + CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY, 1)); + UNSCOPED_INFO("a new access issues one call"); + CHECK(ALImageSlot::sImageBinds == binds + 1); + UNSCOPED_INFO("GL holds the new access"); + CHECK(unitState(2).mAccess == GL_READ_ONLY); + + UNSCOPED_INFO("another level"); + CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY, 0)); + UNSCOPED_INFO("a new level issues one call"); + CHECK(ALImageSlot::sImageBinds == binds + 2); + UNSCOPED_INFO("GL holds the new level"); + CHECK(unitState(2).mLevel == 0); + + UNSCOPED_INFO("another texture"); + CHECK(slot->bind(&mOther, 0, ALImageSlot::READ_ONLY, 0)); + UNSCOPED_INFO("a new texture issues one call"); + CHECK(ALImageSlot::sImageBinds == binds + 3); state = unitState(2); - ensure_equals("GL holds the other texture", state.mTexture, (GLint)mOther.getTexture()); - ensure_equals("at its own format", state.mFormat, GL_RGBA16F); + UNSCOPED_INFO("GL holds the other texture"); + CHECK(state.mTexture == (GLint)mOther.getTexture()); + UNSCOPED_INFO("at its own format"); + CHECK(state.mFormat == GL_RGBA16F); slot->unbind(); - ensure_equals("unbind issues one call", ALImageSlot::sImageBinds, binds + 4); - ensure_equals("GL holds no texture", unitState(2).mTexture, 0); - ensure_equals("nor does the slot", slot->getCurrTexture(), 0u); + UNSCOPED_INFO("unbind issues one call"); + CHECK(ALImageSlot::sImageBinds == binds + 4); + UNSCOPED_INFO("GL holds no texture"); + CHECK(unitState(2).mTexture == 0); + UNSCOPED_INFO("nor does the slot"); + CHECK(slot->getCurrTexture() == 0u); slot->unbind(); - ensure_equals("unbinding nothing issues nothing", ALImageSlot::sImageBinds, binds + 4); + UNSCOPED_INFO("unbinding nothing issues nothing"); + CHECK(ALImageSlot::sImageBinds == binds + 4); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A unit changed behind LLRender's back is put back by refreshState, every part of it. - template<> template<> - void alimageslot_object_t::test<2>() + TEST_CASE_METHOD(alimageslot_data, "alimageslot_gl: refreshState puts back every part of a unit changed behind its back", "[alimageslot_gl]") { setUp(); - ensure("bound", gGL.getImageSlot(3)->bind(&mTarget, 0, ALImageSlot::WRITE_ONLY, 1)); - ensure("other bound", gGL.getImageSlot(4)->bind(&mOther, 0, ALImageSlot::READ_ONLY)); + UNSCOPED_INFO("bound"); + CHECK(gGL.getImageSlot(3)->bind(&mTarget, 0, ALImageSlot::WRITE_ONLY, 1)); + UNSCOPED_INFO("other bound"); + CHECK(gGL.getImageSlot(4)->bind(&mOther, 0, ALImageSlot::READ_ONLY)); glBindImageTexture(3, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R8); glBindImageTexture(4, mTarget.getTexture(), 0, GL_FALSE, 0, GL_READ_WRITE, GL_R32F); - ensure_equals("clobbered", unitState(3).mTexture, 0); + UNSCOPED_INFO("clobbered"); + CHECK(unitState(3).mTexture == 0); gGL.refreshState(); UnitState state = unitState(3); - ensure_equals("the texture put back", state.mTexture, (GLint)mTarget.getTexture()); - ensure_equals("its level put back", state.mLevel, 1); - ensure_equals("its access put back", state.mAccess, GL_WRITE_ONLY); - ensure_equals("its format put back", state.mFormat, GL_R32F); + UNSCOPED_INFO("the texture put back"); + CHECK(state.mTexture == (GLint)mTarget.getTexture()); + UNSCOPED_INFO("its level put back"); + CHECK(state.mLevel == 1); + UNSCOPED_INFO("its access put back"); + CHECK(state.mAccess == GL_WRITE_ONLY); + UNSCOPED_INFO("its format put back"); + CHECK(state.mFormat == GL_R32F); state = unitState(4); - ensure_equals("the other texture put back", state.mTexture, (GLint)mOther.getTexture()); - ensure_equals("its access put back", state.mAccess, GL_READ_ONLY); - ensure_equals("its format put back", state.mFormat, GL_RGBA16F); - - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("the other texture put back"); + CHECK(state.mTexture == (GLint)mOther.getTexture()); + UNSCOPED_INFO("its access put back"); + CHECK(state.mAccess == GL_READ_ONLY); + UNSCOPED_INFO("its format put back"); + CHECK(state.mFormat == GL_RGBA16F); + + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // forgetTextures drops a unit holding a name about to be deleted, and only that one, so the name bound again // afterwards -- which GL is free to hand to a new texture -- is bound rather than skipped. And deleting a target // the viewer's way, through LLImageGL's deferred delete, reaches it. - template<> template<> - void alimageslot_object_t::test<3>() + TEST_CASE_METHOD(alimageslot_data, "alimageslot_gl: forgetTextures drops only the units that held a deleted name", "[alimageslot_gl]") { setUp(); ALImageSlot* slot = gGL.getImageSlot(1); ALImageSlot* kept = gGL.getImageSlot(5); - ensure("bound", slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); - ensure("other bound", kept->bind(&mOther, 0, ALImageSlot::READ_ONLY)); + UNSCOPED_INFO("bound"); + CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); + UNSCOPED_INFO("other bound"); + CHECK(kept->bind(&mOther, 0, ALImageSlot::READ_ONLY)); const U32 name = mTarget.getTexture(); gGL.forgetTextures(1, &name); - ensure_equals("the slot forgot the name", slot->getCurrTexture(), 0u); - ensure_equals("the other slot did not", kept->getCurrTexture(), mOther.getTexture()); + UNSCOPED_INFO("the slot forgot the name"); + CHECK(slot->getCurrTexture() == 0u); + UNSCOPED_INFO("the other slot did not"); + CHECK(kept->getCurrTexture() == mOther.getTexture()); const U32 binds = ALImageSlot::sImageBinds; - ensure("bound again", slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); - ensure_equals("the forgotten name is bound, not skipped", ALImageSlot::sImageBinds, binds + 1); + UNSCOPED_INFO("bound again"); + CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); + UNSCOPED_INFO("the forgotten name is bound, not skipped"); + CHECK(ALImageSlot::sImageBinds == binds + 1); // The viewer's way: the target's textures go on LLImageGL's free list and are deleted some frames on. const U32 other = mOther.getTexture(); @@ -248,85 +283,111 @@ namespace tut { LLImageGL::updateClass(); } - ensure_equals("the deleted texture's slot forgot it", kept->getCurrTexture(), 0u); - ensure_equals("GL dropped it too", unitState(5).mTexture, 0); - ensure_equals("the live one stays", slot->getCurrTexture(), name); - ensure("the deleted name is gone", glIsTexture(other) == GL_FALSE); - - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("the deleted texture's slot forgot it"); + CHECK(kept->getCurrTexture() == 0u); + UNSCOPED_INFO("GL dropped it too"); + CHECK(unitState(5).mTexture == 0); + UNSCOPED_INFO("the live one stays"); + CHECK(slot->getCurrTexture() == name); + UNSCOPED_INFO("the deleted name is gone"); + CHECK(glIsTexture(other) == GL_FALSE); + + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // What an image cannot be bound to: an sRGB texture -- which an LLImageGL made from an image is -- a depth // texture, and a unit past the slots. Each is refused, with nothing issued and the unit unchanged; the same image // with a linear format, and a volume, bind. - template<> template<> - void alimageslot_object_t::test<4>() + TEST_CASE_METHOD(alimageslot_data, "alimageslot_gl: sRGB and depth textures and units past the slots are refused", "[alimageslot_gl]") { setUp(); ALImageSlot* slot = gGL.getImageSlot(6); - ensure("bound", slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); + UNSCOPED_INFO("bound"); + CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); const U32 binds = ALImageSlot::sImageBinds; LLPointer raw = new LLImageRaw(4, 4, 4); std::memset(raw->getData(), 0x80, 4 * 4 * 4); LLPointer srgb = new LLImageGL(raw, false); - ensure_equals("an image's texture is sRGB", srgb->getStorageInternalFormat(), (S32)GL_SRGB8_ALPHA8); - ensure("sRGB refused", !slot->bind(srgb.get(), ALImageSlot::READ_ONLY)); + UNSCOPED_INFO("an image's texture is sRGB"); + CHECK(srgb->getStorageInternalFormat() == (S32)GL_SRGB8_ALPHA8); + UNSCOPED_INFO("sRGB refused"); + CHECK_FALSE(slot->bind(srgb.get(), ALImageSlot::READ_ONLY)); LLRenderTarget depth; - ensure("depth target allocates", depth.allocate(SIZE, SIZE, 0, true)); - ensure("depth refused", !slot->bindManual(depth.getDepth(), 0, false, 0, ALImageSlot::READ_ONLY, + UNSCOPED_INFO("depth target allocates"); + REQUIRE(depth.allocate(SIZE, SIZE, 0, true)); + UNSCOPED_INFO("depth refused"); + CHECK_FALSE(slot->bindManual(depth.getDepth(), 0, false, 0, ALImageSlot::READ_ONLY, GL_DEPTH_COMPONENT24)); depth.release(); - ensure("past the slots refused", !gGL.getImageSlot(AL_NUM_IMAGE_SLOTS)->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); + UNSCOPED_INFO("past the slots refused"); + CHECK_FALSE(gGL.getImageSlot(AL_NUM_IMAGE_SLOTS)->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); - ensure_equals("nothing issued", ALImageSlot::sImageBinds, binds); - ensure_equals("the unit unchanged", unitState(6).mTexture, (GLint)mTarget.getTexture()); - ensure_equals("and the slot", slot->getCurrTexture(), mTarget.getTexture()); + UNSCOPED_INFO("nothing issued"); + CHECK(ALImageSlot::sImageBinds == binds); + UNSCOPED_INFO("the unit unchanged"); + CHECK(unitState(6).mTexture == (GLint)mTarget.getTexture()); + UNSCOPED_INFO("and the slot"); + CHECK(slot->getCurrTexture() == mTarget.getTexture()); LLPointer linear = new LLImageGL(4, 4, 4, false); linear->setExplicitFormat(GL_RGBA8, GL_RGBA); - ensure("linear image created", linear->createGLTexture(0, raw.get())); - ensure("linear bound", slot->bind(linear.get(), ALImageSlot::READ_ONLY)); + UNSCOPED_INFO("linear image created"); + CHECK(linear->createGLTexture(0, raw.get())); + UNSCOPED_INFO("linear bound"); + CHECK(slot->bind(linear.get(), ALImageSlot::READ_ONLY)); UnitState state = unitState(6); - ensure_equals("GL holds the image", state.mTexture, (GLint)linear->getTexName()); - ensure_equals("at RGBA8", state.mFormat, GL_RGBA8); - ensure_equals("one layer", state.mLayered, GL_FALSE); + UNSCOPED_INFO("GL holds the image"); + CHECK(state.mTexture == (GLint)linear->getTexName()); + UNSCOPED_INFO("at RGBA8"); + CHECK(state.mFormat == GL_RGBA8); + UNSCOPED_INFO("one layer"); + CHECK(state.mLayered == GL_FALSE); LLPointer volume = new ALTexture3D(); - ensure("volume allocates", volume->allocate(4, 4, 4, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, nullptr)); - ensure("volume bound", slot->bind(volume.get(), ALImageSlot::WRITE_ONLY)); + UNSCOPED_INFO("volume allocates"); + CHECK(volume->allocate(4, 4, 4, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, nullptr)); + UNSCOPED_INFO("volume bound"); + CHECK(slot->bind(volume.get(), ALImageSlot::WRITE_ONLY)); state = unitState(6); - ensure_equals("GL holds the volume", state.mTexture, (GLint)volume->getGLName()); - ensure_equals("every slice", state.mLayered, GL_TRUE); - ensure_equals("at its own format", state.mFormat, GL_RGBA16F); + UNSCOPED_INFO("GL holds the volume"); + CHECK(state.mTexture == (GLint)volume->getGLName()); + UNSCOPED_INFO("every slice"); + CHECK(state.mLayered == GL_TRUE); + UNSCOPED_INFO("at its own format"); + CHECK(state.mFormat == GL_RGBA16F); slot->unbind(); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // An R32UI target and an R32UI image, each the r32ui image of a kernel that loads every texel, changes it and // stores it back. The target's texels start where LLRenderTarget::clear put them and the image's where its upload // did, and both come back changed exactly, past what a float could carry. - template<> template<> - void alimageslot_object_t::test<5>() + TEST_CASE_METHOD(alimageslot_data, "alimageslot_gl: a kernel changes R32UI target and image texels exactly", "[alimageslot_gl]") { setUp(); constexpr U32 W = 13; constexpr U32 H = 7; const GLuint cs = ll_test::compileTestShader(GL_COMPUTE_SHADER, kUintKernel); - ensure("kernel compiles", cs != 0); + UNSCOPED_INFO("kernel compiles"); + CHECK(cs != 0); mKernel = glCreateProgram(); glAttachShader(mKernel, cs); glLinkProgram(mKernel); glDeleteShader(cs); GLint linked = GL_FALSE; glGetProgramiv(mKernel, GL_LINK_STATUS, &linked); - ensure("kernel links", linked == GL_TRUE); + UNSCOPED_INFO("kernel links"); + CHECK(linked == GL_TRUE); - ensure("R32UI target allocates", mUint.allocate(W, H, GL_R32UI)); + UNSCOPED_INFO("R32UI target allocates"); + CHECK(mUint.allocate(W, H, GL_R32UI)); mUint.bindTarget(); gGL.setClearColor(LLColor4(5.f, 0.f, 0.f, 0.f)); mUint.clear(); @@ -334,8 +395,10 @@ namespace tut ALImageSlot* slot = gGL.getImageSlot(0); glUseProgram(mKernel); - ensure("target bound", slot->bind(&mUint, 0, ALImageSlot::READ_WRITE)); - ensure_equals("as R32UI", unitState(0).mFormat, GL_R32UI); + UNSCOPED_INFO("target bound"); + CHECK(slot->bind(&mUint, 0, ALImageSlot::READ_WRITE)); + UNSCOPED_INFO("as R32UI"); + CHECK(unitState(0).mFormat == GL_R32UI); glDispatchCompute((W + 7) / 8, (H + 7) / 8, 1); gGL.memoryBarrier(LLRender::BARRIER_TEXTURE_UPDATE); const std::vector target = readUints(mUint.getTexture(), W, H); @@ -347,9 +410,12 @@ namespace tut } LLPointer image = new LLImageGL(W, H, 1, /*usemipmaps=*/false); image->setExplicitFormat(GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT); - ensure("R32UI image created", image->createGLTexture(0, reinterpret_cast(values.data()))); - ensure("image bound", slot->bind(image.get(), ALImageSlot::READ_WRITE)); - ensure_equals("as R32UI too", unitState(0).mFormat, GL_R32UI); + UNSCOPED_INFO("R32UI image created"); + CHECK(image->createGLTexture(0, reinterpret_cast(values.data()))); + UNSCOPED_INFO("image bound"); + CHECK(slot->bind(image.get(), ALImageSlot::READ_WRITE)); + UNSCOPED_INFO("as R32UI too"); + CHECK(unitState(0).mFormat == GL_R32UI); glDispatchCompute((W + 7) / 8, (H + 7) / 8, 1); gGL.memoryBarrier(LLRender::BARRIER_TEXTURE_UPDATE); const std::vector held = readUints(image->getTexName(), W, H); @@ -362,10 +428,13 @@ namespace tut { const std::string at = " at " + std::to_string(x) + "," + std::to_string(y); const size_t i = (size_t)y * W + x; - ensure_equals("target" + at, target[i], 5u * 3u + x + y * 100u); - ensure_equals("image" + at, held[i], values[i] * 3u + x + y * 100u); + UNSCOPED_INFO("target" + at); + REQUIRE(target[i] == 5u * 3u + x + y * 100u); + UNSCOPED_INFO("image" + at); + REQUIRE(held[i] == values[i] * 3u + x + y * 100u); } } - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } } diff --git a/indra/llrender/tests/alinfiniteprojection_gl_test.cpp b/indra/llrender/tests/alinfiniteprojection_gl_test.cpp index 68d6ae21fe..af9e0aada9 100644 --- a/indra/llrender/tests/alinfiniteprojection_gl_test.cpp +++ b/indra/llrender/tests/alinfiniteprojection_gl_test.cpp @@ -34,16 +34,15 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include +#include "altestmatchers.h" #include #include // Numbers and bounds from an independent double-precision reference of the projection. -namespace tut +namespace { - namespace - { constexpr S32 SIZE = 8; constexpr F32 NEAR_PLANE = 0.1f; constexpr F32 FOV = 1.0f; @@ -128,12 +127,16 @@ namespace tut { if (!gGLManager.mHasClipControl) { - skip("no clip control: reverse-Z is unavailable on this context"); + SKIP("no clip control: reverse-Z is unavailable on this context"); } const GLuint vs = ll_test::compileTestShader(GL_VERTEX_SHADER, kVertex); const GLuint fs = ll_test::compileTestShader(GL_FRAGMENT_SHADER, kFragment); - ensure("shaders compile", vs && fs); + { + INFO("shaders compile"); + REQUIRE(vs); + REQUIRE(fs); + } mProgram = glCreateProgram(); glAttachShader(mProgram, vs); glAttachShader(mProgram, fs); @@ -142,7 +145,8 @@ namespace tut glDeleteShader(fs); GLint linked = GL_FALSE; glGetProgramiv(mProgram, GL_LINK_STATUS, &linked); - ensure("program links", linked == GL_TRUE); + UNSCOPED_INFO("program links"); + REQUIRE(linked == GL_TRUE); glGenVertexArrays(1, &mVAO); glGenTextures(1, &mDepth); @@ -156,7 +160,8 @@ namespace tut glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, mDepth, 0); glDrawBuffer(GL_NONE); glReadBuffer(GL_NONE); - ensure_equals("complete", (U32)glCheckFramebufferStatus(GL_FRAMEBUFFER), (U32)GL_FRAMEBUFFER_COMPLETE); + UNSCOPED_INFO("complete"); + REQUIRE((U32)glCheckFramebufferStatus(GL_FRAMEBUFFER) == (U32)GL_FRAMEBUFFER_COMPLETE); glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE); glViewport(0, 0, SIZE, SIZE); @@ -195,74 +200,61 @@ namespace tut GLuint mProgram = 0; }; - typedef test_group alinfiniteprojection_t; - typedef alinfiniteprojection_t::object alinfiniteprojection_object_t; - tut::alinfiniteprojection_t tut_alinfiniteprojection("alinfiniteprojection_gl"); - } // The matrix: recognised as infinite, stores near / distance, and its inverse gives w = 0 at the cleared // depth, which the reconstruction floor turns into MAX_RECONSTRUCT_DISTANCE. Finite and forward matrices are not. - template<> template<> - void alinfiniteprojection_object_t::test<1>() + TEST_CASE_METHOD(alinfiniteprojection_data, "alinfiniteprojection_gl: the infinite matrix stores near over distance and its far end is floored", "[alinfiniteprojection_gl]") { const LLMatrix4a proj = infinite(); - ensure("infinite", al_projection_is_infinite(proj)); - ensure("finite reversed is not", !al_projection_is_infinite(al_perspective(FOV, 1.f, NEAR_PLANE, 1024.f))); - ensure("ortho is not", !al_projection_is_infinite(al_ortho(-1.f, 1.f, -1.f, 1.f, 0.f, 10.f))); + UNSCOPED_INFO("infinite"); + CHECK(al_projection_is_infinite(proj)); + UNSCOPED_INFO("finite reversed is not"); + CHECK_FALSE(al_projection_is_infinite(al_perspective(FOV, 1.f, NEAR_PLANE, 1024.f))); + UNSCOPED_INFO("ortho is not"); + CHECK_FALSE(al_projection_is_infinite(al_ortho(-1.f, 1.f, -1.f, 1.f, 0.f, 10.f))); for (F32 dist : DISTANCES) { LLVector4a clip; proj.transform4(LLVector4a(0.f, 0.f, -dist, 1.f), clip); - ensure_equals("clip z is the near plane", clip[2], NEAR_PLANE); - ensure_equals("clip w is the distance", clip[3], dist); + UNSCOPED_INFO("clip z is the near plane"); + REQUIRE(clip[2] == NEAR_PLANE); + UNSCOPED_INFO("clip w is the distance"); + REQUIRE(clip[3] == dist); } LLMatrix4a inv = proj; inv.invert(); LLVector4a far_point; inv.transform4(LLVector4a(0.f, 0.f, 0.f, 1.f), far_point); - ensure("cleared depth unprojects to w = 0", fabsf(far_point[3]) < 1e-12f); - ensure_approximately_equals("floored to MAX_RECONSTRUCT_DISTANCE", reconstruct(inv, 0.f), MAX_RECONSTRUCT_DISTANCE, 8); + UNSCOPED_INFO("cleared depth unprojects to w = 0"); + CHECK(fabsf(far_point[3]) < 1e-12f); + UNSCOPED_INFO("floored to MAX_RECONSTRUCT_DISTANCE"); + CHECK_THAT(reconstruct(inv, 0.f), ll_test::WithinBits(MAX_RECONSTRUCT_DISTANCE, 8)); // The floor leaves a finite projection alone: its cleared depth still reconstructs at its far plane. LLMatrix4a inv_finite = al_perspective(FOV, 1.f, NEAR_PLANE, 1024.f); inv_finite.invert(); LLVector4a raw; inv_finite.transform4(LLVector4a(0.f, 0.f, 0.f, 1.f), raw); - ensure_equals("finite far untouched by the floor", reconstruct(inv_finite, 0.f), -raw[2] / raw[3]); - ensure("finite far is about 1024 m", fabsf(-raw[2] / raw[3] - 1024.f) < 1.f); + UNSCOPED_INFO("finite far untouched by the floor"); + CHECK(reconstruct(inv_finite, 0.f) == -raw[2] / raw[3]); + UNSCOPED_INFO("finite far is about 1024 m"); + CHECK(fabsf(-raw[2] / raw[3] - 1024.f) < 1.f); LLRender::sReverseZ = false; const LLMatrix4a forward = al_perspective(FOV, 1.f, NEAR_PLANE, std::numeric_limits::infinity()); - ensure("forward falls back to a finite plane", !al_projection_is_infinite(forward) && std::isfinite(forward.getColumn<2>()[2])); - LLRender::sReverseZ = true; - } - - // Forward-Z cannot hold an infinite plane in 24-bit depth, so an infinite request falls back to the old - // fixed plane rather than MAX_FAR_PLANE, and depth beyond it is clipped as before. - template<> template<> - void alinfiniteprojection_object_t::test<4>() - { - LLRender::sReverseZ = false; - const LLMatrix4a forward = al_perspective(FOV, 1.f, NEAR_PLANE, std::numeric_limits::infinity()); - const LLMatrix4a legacy = al_perspective(FOV, 1.f, NEAR_PLANE, FINITE_PROJECTION_FAR); - LLRender::sReverseZ = true; - for (S32 i = 0; i < 16; ++i) { - ensure_equals("same as the old fixed plane", forward.getF32ptr()[i], legacy.getF32ptr()[i]); + INFO("forward falls back to a finite plane"); + CHECK_FALSE(al_projection_is_infinite(forward)); + CHECK(std::isfinite(forward.getColumn<2>()[2])); } - LLVector4a clip; - forward.transform4(LLVector4a(0.f, 0.f, -FINITE_PROJECTION_FAR, 1.f), clip); - ensure("far plane at the old fixed plane", fabsf(clip[2] / clip[3] - 1.f) < 1e-5f); - forward.transform4(LLVector4a(0.f, 0.f, -2.f * FINITE_PROJECTION_FAR, 1.f), clip); - ensure("past it is clipped", clip[2] / clip[3] > 1.f); + LLRender::sReverseZ = true; } // Draw at each distance: the stored depth is near / distance (oracle), nothing is clipped far, and the // reconstructed distance is within the oracle's bound (0.29 mm, 3.9 mm, 9.3 mm, 45 mm). - template<> template<> - void alinfiniteprojection_object_t::test<2>() + TEST_CASE_METHOD(alinfiniteprojection_data, "alinfiniteprojection_gl: drawn depth is near over distance and round-trips within the bound", "[alinfiniteprojection_gl]") { setUp(); const LLMatrix4a proj = infinite(); @@ -277,22 +269,26 @@ namespace tut glClear(GL_DEPTH_BUFFER_BIT); draw(proj, dist); const F32 d = centreDepth(); - ensure("drawn at " + at, d > 0.f); - ensure("stored depth at " + at, fabsf(d - oracle_depth[i]) <= oracle_depth[i] * 1e-6f); + UNSCOPED_INFO("drawn at " + at); + REQUIRE(d > 0.f); + UNSCOPED_INFO("stored depth at " + at); + REQUIRE(fabsf(d - oracle_depth[i]) <= oracle_depth[i] * 1e-6f); const F64 err = fabs((F64)reconstruct(inv, d) - dist); - ensure("round trip at " + at + ": " + std::to_string(err * 1000.0) + " mm", err <= bound(dist, d)); + UNSCOPED_INFO("round trip at " + at + ": " + std::to_string(err * 1000.0) + " mm"); + REQUIRE(err <= bound(dist, d)); } // Past every reach and still not clipped: 1000 km. glClear(GL_DEPTH_BUFFER_BIT); draw(proj, 1.0e6f); - ensure("1000 km is not clipped", centreDepth() > 0.f); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + UNSCOPED_INFO("1000 km is not clipped"); + CHECK(centreDepth() > 0.f); + UNSCOPED_INFO("no GL error"); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } // Two surfaces four depth steps apart at each distance: the nearer one wins whichever is drawn first. - template<> template<> - void alinfiniteprojection_object_t::test<3>() + TEST_CASE_METHOD(alinfiniteprojection_data, "alinfiniteprojection_gl: surfaces four depth steps apart resolve in either draw order", "[alinfiniteprojection_gl]") { setUp(); const LLMatrix4a proj = infinite(); @@ -308,21 +304,45 @@ namespace tut const F32 d_far = centreDepth(); draw(proj, nearer); const F32 d_near = centreDepth(); - ensure("nearer drawn second wins at " + at, d_near > d_far); + UNSCOPED_INFO("nearer drawn second wins at " + at); + REQUIRE(d_near > d_far); glClear(GL_DEPTH_BUFFER_BIT); draw(proj, nearer); draw(proj, dist); - ensure_equals("farther drawn second loses at " + at, centreDepth(), d_near); + UNSCOPED_INFO("farther drawn second loses at " + at); + REQUIRE(centreDepth() == d_near); } - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + UNSCOPED_INFO("no GL error"); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); + } + + // Forward-Z cannot hold an infinite plane in 24-bit depth, so an infinite request falls back to the old + // fixed plane rather than MAX_FAR_PLANE, and depth beyond it is clipped as before. + TEST_CASE_METHOD(alinfiniteprojection_data, "alinfiniteprojection_gl: forward-Z falls back to the old fixed plane and clips past it", "[alinfiniteprojection_gl]") + { + LLRender::sReverseZ = false; + const LLMatrix4a forward = al_perspective(FOV, 1.f, NEAR_PLANE, std::numeric_limits::infinity()); + const LLMatrix4a legacy = al_perspective(FOV, 1.f, NEAR_PLANE, FINITE_PROJECTION_FAR); + LLRender::sReverseZ = true; + for (S32 i = 0; i < 16; ++i) + { + UNSCOPED_INFO("same as the old fixed plane"); + REQUIRE(forward.getF32ptr()[i] == legacy.getF32ptr()[i]); + } + LLVector4a clip; + forward.transform4(LLVector4a(0.f, 0.f, -FINITE_PROJECTION_FAR, 1.f), clip); + UNSCOPED_INFO("far plane at the old fixed plane"); + CHECK(fabsf(clip[2] / clip[3] - 1.f) < 1e-5f); + forward.transform4(LLVector4a(0.f, 0.f, -2.f * FINITE_PROJECTION_FAR, 1.f), clip); + UNSCOPED_INFO("past it is clipped"); + CHECK(clip[2] / clip[3] > 1.f); } // The sky draws last, on the far plane, writing no depth, and the depth test spares it every pixel the world // covered (skyV.glsl). Under the infinite projection the far plane is the cleared depth exactly and geometry as // far out as reconstruction reaches still stores more, so the sky passes where nothing was drawn and nowhere else. - template<> template<> - void alinfiniteprojection_object_t::test<5>() + TEST_CASE_METHOD(alinfiniteprojection_data, "alinfiniteprojection_gl: the sky on the far plane passes only where nothing was drawn", "[alinfiniteprojection_gl]") { setUp(); const LLMatrix4a proj = infinite(); @@ -343,30 +363,35 @@ namespace tut }; glClear(GL_DEPTH_BUFFER_BIT); - ensure_equals("the sky fills an empty frame", sky_samples(), (GLuint)(SIZE * SIZE)); - ensure_equals("the sky leaves the cleared depth", centreDepth(), 0.f); + UNSCOPED_INFO("the sky fills an empty frame"); + CHECK(sky_samples() == (GLuint)(SIZE * SIZE)); + UNSCOPED_INFO("the sky leaves the cleared depth"); + CHECK(centreDepth() == 0.f); for (F32 dist : { 1000.f, 100000.f, MAX_RECONSTRUCT_DISTANCE }) { const std::string at = std::to_string((S32)dist) + " m"; glClear(GL_DEPTH_BUFFER_BIT); draw(proj, dist); - ensure("geometry at " + at + " stores more than the far plane", centreDepth() > 0.f); - ensure_equals("the sky stays behind geometry at " + at, sky_samples(), (GLuint)0); + UNSCOPED_INFO("geometry at " + at + " stores more than the far plane"); + REQUIRE(centreDepth() > 0.f); + UNSCOPED_INFO("the sky stays behind geometry at " + at); + REQUIRE(sky_samples() == (GLuint)0); } glDeleteQueries(1, &query); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + UNSCOPED_INFO("no GL error"); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } // The haze pass draws on the far plane too (hazeV.glsl), testing the other way (LLPipeline::doAtmospherics): only // in front of the far plane, so it shades every pixel geometry covers, however far out, and no pixel of sky. - template<> template<> - void alinfiniteprojection_object_t::test<6>() + TEST_CASE_METHOD(alinfiniteprojection_data, "alinfiniteprojection_gl: the haze pass on the far plane shades all geometry and no sky", "[alinfiniteprojection_gl]") { setUp(); const LLMatrix4a proj = infinite(); - ensure_equals("in front of the far plane is LESS under reverse-Z", (U32)LLGLDepthTest::remap(GL_GREATER), (U32)GL_LESS); + UNSCOPED_INFO("in front of the far plane is LESS under reverse-Z"); + CHECK((U32)LLGLDepthTest::remap(GL_GREATER) == (U32)GL_LESS); GLuint query = 0; glGenQueries(1, &query); const auto haze_samples = [&]() @@ -384,14 +409,16 @@ namespace tut }; glClear(GL_DEPTH_BUFFER_BIT); - ensure_equals("no haze on an empty frame", haze_samples(), (GLuint)0); + UNSCOPED_INFO("no haze on an empty frame"); + CHECK(haze_samples() == (GLuint)0); for (F32 dist : { 1.f, 1000.f, 100000.f, MAX_RECONSTRUCT_DISTANCE }) { const std::string at = std::to_string((S32)dist) + " m"; glClear(GL_DEPTH_BUFFER_BIT); draw(proj, dist); - ensure_equals("haze over all of the geometry at " + at, haze_samples(), (GLuint)(SIZE * SIZE)); + UNSCOPED_INFO("haze over all of the geometry at " + at); + REQUIRE(haze_samples() == (GLuint)(SIZE * SIZE)); } // Pixel by pixel: geometry over the left half only, sky over the right. @@ -399,17 +426,18 @@ namespace tut glViewport(0, 0, SIZE / 2, SIZE); draw(proj, MAX_RECONSTRUCT_DISTANCE); glViewport(0, 0, SIZE, SIZE); - ensure_equals("haze over the geometry's half alone", haze_samples(), (GLuint)(SIZE * SIZE / 2)); + UNSCOPED_INFO("haze over the geometry's half alone"); + CHECK(haze_samples() == (GLuint)(SIZE * SIZE / 2)); glDeleteQueries(1, &query); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + UNSCOPED_INFO("no GL error"); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } // The soften pass draws on the far plane twice (softenLightV.glsl, LLPipeline::renderDeferredLighting): its lit // program in front of the far plane, as the haze does, and its sky program on it. Between them every pixel is // shaded once, by the program for what it is, however far out the geometry lies. - template<> template<> - void alinfiniteprojection_object_t::test<7>() + TEST_CASE_METHOD(alinfiniteprojection_data, "alinfiniteprojection_gl: the soften pass shades every pixel once as lit or as sky", "[alinfiniteprojection_gl]") { setUp(); const LLMatrix4a proj = infinite(); @@ -430,16 +458,20 @@ namespace tut }; glClear(GL_DEPTH_BUFFER_BIT); - ensure_equals("nothing lit on an empty frame", samples(GL_GREATER), (GLuint)0); - ensure_equals("all sky on an empty frame", samples(GL_EQUAL), (GLuint)(SIZE * SIZE)); + UNSCOPED_INFO("nothing lit on an empty frame"); + CHECK(samples(GL_GREATER) == (GLuint)0); + UNSCOPED_INFO("all sky on an empty frame"); + CHECK(samples(GL_EQUAL) == (GLuint)(SIZE * SIZE)); for (F32 dist : { 1.f, 1000.f, 100000.f, MAX_RECONSTRUCT_DISTANCE }) { const std::string at = std::to_string((S32)dist) + " m"; glClear(GL_DEPTH_BUFFER_BIT); draw(proj, dist); - ensure_equals("all lit over geometry at " + at, samples(GL_GREATER), (GLuint)(SIZE * SIZE)); - ensure_equals("no sky over geometry at " + at, samples(GL_EQUAL), (GLuint)0); + UNSCOPED_INFO("all lit over geometry at " + at); + REQUIRE(samples(GL_GREATER) == (GLuint)(SIZE * SIZE)); + UNSCOPED_INFO("no sky over geometry at " + at); + REQUIRE(samples(GL_EQUAL) == (GLuint)0); } // Pixel by pixel: geometry at the reconstruction floor over the left half only, sky over the right. @@ -449,10 +481,13 @@ namespace tut glViewport(0, 0, SIZE, SIZE); const GLuint lit = samples(GL_GREATER); const GLuint sky = samples(GL_EQUAL); - ensure_equals("lit over the geometry's half", lit, (GLuint)(SIZE * SIZE / 2)); - ensure_equals("every pixel shaded once", lit + sky, (GLuint)(SIZE * SIZE)); + UNSCOPED_INFO("lit over the geometry's half"); + CHECK(lit == (GLuint)(SIZE * SIZE / 2)); + UNSCOPED_INFO("every pixel shaded once"); + CHECK(lit + sky == (GLuint)(SIZE * SIZE)); glDeleteQueries(1, &query); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + UNSCOPED_INFO("no GL error"); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } } diff --git a/indra/llrender/tests/alinstanceddraw_gl_test.cpp b/indra/llrender/tests/alinstanceddraw_gl_test.cpp index 1b5614b09d..c7c0a0b886 100644 --- a/indra/llrender/tests/alinstanceddraw_gl_test.cpp +++ b/indra/llrender/tests/alinstanceddraw_gl_test.cpp @@ -33,12 +33,10 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include -namespace tut +namespace { - namespace - { constexpr S32 W = 16; constexpr S32 H = 16; @@ -96,18 +94,26 @@ namespace tut { const GLuint vs = ll_test::compileTestShader(GL_VERTEX_SHADER, kGridVertex); const GLuint fs = ll_test::compileTestShader(GL_FRAGMENT_SHADER, kGridFragment); - ensure("grid shaders compile", vs && fs); + { + INFO("grid shaders compile"); + REQUIRE(vs); + REQUIRE(fs); + } mProgram.mProgramObject = glCreateProgram(); mProgram.mName = "attribute-less grid (test)"; mProgram.attachObject(vs); mProgram.attachObject(fs); - ensure("grid program links", mProgram.mapAttributes()); + UNSCOPED_INFO("grid program links"); + REQUIRE(mProgram.mapAttributes()); glDeleteShader(vs); glDeleteShader(fs); - ensure("grid program maps", mProgram.mapUniforms()); - ensure_equals("the grid program has no vertex inputs", mProgram.mAttributeMask, 0U); + UNSCOPED_INFO("grid program maps"); + REQUIRE(mProgram.mapUniforms()); + UNSCOPED_INFO("the grid program has no vertex inputs"); + REQUIRE(mProgram.mAttributeMask == 0U); - ensure("target allocates", mTarget.allocate(W, H, GL_RGBA8)); + UNSCOPED_INFO("target allocates"); + REQUIRE(mTarget.allocate(W, H, GL_RGBA8)); mTarget.bindTarget(); gGL.setClearColor(LLColor4::black); mTarget.clear(); @@ -148,36 +154,34 @@ namespace tut LLRenderTarget mTarget; }; - typedef test_group alinstanceddraw_t; - typedef alinstanceddraw_t::object alinstanceddraw_object_t; - tut::alinstanceddraw_t tut_alinstanceddraw("alinstanceddraw_gl"); - } // Three instances of a four-vertex strip cover the first three cells of the bottom row and nothing else. - template<> template<> - void alinstanceddraw_object_t::test<1>() + TEST_CASE_METHOD(alinstanceddraw_data, "alinstanceddraw_gl: three instanced strips cover the bottom row's first three cells only", "[alinstanceddraw_gl]") { setUp(); gGL.loadMatrix(grid(0.f)); gGL.drawArraysInstanced(LLRender::TRIANGLE_STRIP, 0, 4, 3); - ensure_equals("cells covered", covered(), 0x7U); + UNSCOPED_INFO("cells covered"); + CHECK(covered() == 0x7U); } // The draw syncs the matrices: a projection loaded after one draw places the next. The raw draw, which does not // sync, still places it by the projection the first draw left in the block. - template<> template<> - void alinstanceddraw_object_t::test<2>() + TEST_CASE_METHOD(alinstanceddraw_data, "alinstanceddraw_gl: drawArraysInstanced syncs the matrices and a raw draw does not", "[alinstanceddraw_gl]") { setUp(); gGL.loadMatrix(grid(0.f)); gGL.drawArraysInstanced(LLRender::TRIANGLE_STRIP, 0, 4, 1); - ensure_equals("first draw on the bottom row", covered(), 0x1U); + UNSCOPED_INFO("first draw on the bottom row"); + CHECK(covered() == 0x1U); gGL.loadMatrix(grid(2.f)); glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, 2); - ensure_equals("the raw draw lands where the last synced projection put it", covered(), 0x3U); + UNSCOPED_INFO("the raw draw lands where the last synced projection put it"); + CHECK(covered() == 0x3U); gGL.drawArraysInstanced(LLRender::TRIANGLE_STRIP, 0, 4, 2); - ensure_equals("the synced draw lands on the third row", covered(), 0x3U | 0x300U); + UNSCOPED_INFO("the synced draw lands on the third row"); + CHECK(covered() == (0x3U | 0x300U)); } } diff --git a/indra/llrender/tests/alrenderstate_gl_test.cpp b/indra/llrender/tests/alrenderstate_gl_test.cpp index d17807e860..460d4c6f19 100644 --- a/indra/llrender/tests/alrenderstate_gl_test.cpp +++ b/indra/llrender/tests/alrenderstate_gl_test.cpp @@ -34,12 +34,10 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include -namespace tut +namespace { - namespace - { constexpr U32 SIZE = 4; const LLColor4U RED(255, 0, 0, 255); @@ -84,39 +82,38 @@ namespace tut void setUp() { - ensure("target allocates", mTarget.allocate(SIZE, SIZE, GL_RGBA8)); + UNSCOPED_INFO("target allocates"); + REQUIRE(mTarget.allocate(SIZE, SIZE, GL_RGBA8)); } ll_test::SharedGLScope mGL; LLRenderTarget mTarget; }; - typedef test_group alrenderstate_t; - typedef alrenderstate_t::object alrenderstate_object_t; - tut::alrenderstate_t tut_alrenderstate("alrenderstate_gl"); - } // The clear colour a target's clear fills with is the one gGL was given, and GL holds what gGL says it does. - template<> template<> - void alrenderstate_object_t::test<1>() + TEST_CASE_METHOD(alrenderstate_data, "alrenderstate_gl: a target's clear fills with the colour gGL was given", "[alrenderstate_gl]") { setUp(); mTarget.bindTarget(); gGL.setClearColor(LLColor4::red); - ensure("GL holds the cached colour", glClearValue() == gGL.getClearColor()); + UNSCOPED_INFO("GL holds the cached colour"); + CHECK(glClearValue() == gGL.getClearColor()); mTarget.clear(); - ensure_equals("cleared red", pixel(1, 1), RED); + UNSCOPED_INFO("cleared red"); + CHECK(pixel(1, 1) == RED); gGL.setClearColor(LLColor4::green); mTarget.clear(); - ensure_equals("cleared green", pixel(1, 1), GREEN); + UNSCOPED_INFO("cleared green"); + CHECK(pixel(1, 1) == GREEN); mTarget.flush(); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + UNSCOPED_INFO("no GL error"); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } // refreshState is for state changed behind gGL's back: it puts back what gGL holds, where the setters would see // their cache agreeing and issue nothing. - template<> template<> - void alrenderstate_object_t::test<2>() + TEST_CASE_METHOD(alrenderstate_data, "alrenderstate_gl: refreshState puts back the clear colour and scissor box gGL holds", "[alrenderstate_gl]") { gGL.setClearColor(LLColor4::green); gGL.setScissor(1, 1, 2, 2); @@ -125,54 +122,28 @@ namespace tut glScissor(0, 0, 1, 1); gGL.setClearColor(LLColor4::green); gGL.setScissor(1, 1, 2, 2); - ensure("an agreeing cache issues nothing", glClearValue() == LLColor4::red); + UNSCOPED_INFO("an agreeing cache issues nothing"); + CHECK(glClearValue() == LLColor4::red); gGL.refreshState(); - ensure("refreshState restores the clear colour", glClearValue() == LLColor4::green); + UNSCOPED_INFO("refreshState restores the clear colour"); + CHECK(glClearValue() == LLColor4::green); S32 box[4]; glScissorBox(box); - ensure("refreshState restores the scissor box", box[0] == 1 && box[1] == 1 && box[2] == 2 && box[3] == 2); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); - } - - // And the patch size, on a context with tessellation to have one. - template<> template<> - void alrenderstate_object_t::test<4>() - { - if (!glPatchParameteri) { - skip("this context has no tessellation"); + INFO("refreshState restores the scissor box"); + CHECK(box[0] == 1); + CHECK(box[1] == 1); + CHECK(box[2] == 2); + CHECK(box[3] == 2); } - - // Handed back however the test ends: the shared context outlives it, and its scope does not keep this. - struct PatchSizeRestore - { - GLint mFound = 0; - PatchSizeRestore() { glGetIntegerv(GL_PATCH_VERTICES, &mFound); } - ~PatchSizeRestore() - { - gGL.setPatchVertices((U32)mFound); - glPatchParameteri(GL_PATCH_VERTICES, mFound); - } - } restore; - - gGL.setPatchVertices(4); - glPatchParameteri(GL_PATCH_VERTICES, 3); - gGL.setPatchVertices(4); - GLint patch = 0; - glGetIntegerv(GL_PATCH_VERTICES, &patch); - ensure_equals("an agreeing cache issues nothing", (S32)patch, 3); - - gGL.refreshState(); - glGetIntegerv(GL_PATCH_VERTICES, &patch); - ensure_equals("refreshState restores the patch size", (S32)patch, 4); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + UNSCOPED_INFO("no GL error"); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } // LLGLSScissor cuts a clear to its box with the test on, and hands back the box and the test it found, nested // scopes included. - template<> template<> - void alrenderstate_object_t::test<3>() + TEST_CASE_METHOD(alrenderstate_data, "alrenderstate_gl: LLGLSScissor cuts a clear to its box and hands back what it found", "[alrenderstate_gl]") { setUp(); mTarget.bindTarget(); @@ -182,28 +153,88 @@ namespace tut { LLGLSScissor left(0, 0, SIZE / 2, SIZE); - ensure("the test is on in the scope", glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE); + UNSCOPED_INFO("the test is on in the scope"); + CHECK(glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE); gGL.setClearColor(LLColor4::green); mTarget.clear(); { LLGLSScissor corner(0, 0, 1, 1); S32 box[4]; gGL.getScissor(box); - ensure("the inner scope's box", box[0] == 0 && box[1] == 0 && box[2] == 1 && box[3] == 1); + { + INFO("the inner scope's box"); + CHECK(box[0] == 0); + CHECK(box[1] == 0); + CHECK(box[2] == 1); + CHECK(box[3] == 1); + } } S32 box[4]; glScissorBox(box); - ensure("the outer box is back", box[0] == 0 && box[1] == 0 && box[2] == (S32)SIZE / 2 && box[3] == (S32)SIZE); - ensure("the test is still on", glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE); + { + INFO("the outer box is back"); + CHECK(box[0] == 0); + CHECK(box[1] == 0); + CHECK(box[2] == (S32)SIZE / 2); + CHECK(box[3] == (S32)SIZE); + } + UNSCOPED_INFO("the test is still on"); + CHECK(glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE); } - ensure_equals("inside the box: cleared green", pixel(0, 1), GREEN); - ensure_equals("outside the box: still red", pixel(SIZE - 1, 1), RED); - ensure("the test is off again", glIsEnabled(GL_SCISSOR_TEST) == GL_FALSE); + UNSCOPED_INFO("inside the box: cleared green"); + CHECK(pixel(0, 1) == GREEN); + UNSCOPED_INFO("outside the box: still red"); + CHECK(pixel(SIZE - 1, 1) == RED); + UNSCOPED_INFO("the test is off again"); + CHECK(glIsEnabled(GL_SCISSOR_TEST) == GL_FALSE); S32 box[4]; glScissorBox(box); - ensure("the first box is back", box[0] == 0 && box[1] == 0 && box[2] == (S32)SIZE && box[3] == (S32)SIZE); + { + INFO("the first box is back"); + CHECK(box[0] == 0); + CHECK(box[1] == 0); + CHECK(box[2] == (S32)SIZE); + CHECK(box[3] == (S32)SIZE); + } mTarget.flush(); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + UNSCOPED_INFO("no GL error"); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); + } + + // And the patch size, on a context with tessellation to have one. + TEST_CASE_METHOD(alrenderstate_data, "alrenderstate_gl: refreshState puts back the patch size gGL holds", "[alrenderstate_gl]") + { + if (!glPatchParameteri) + { + SKIP("this context has no tessellation"); + } + + // Handed back however the test ends: the shared context outlives it, and its scope does not keep this. + struct PatchSizeRestore + { + GLint mFound = 0; + PatchSizeRestore() { glGetIntegerv(GL_PATCH_VERTICES, &mFound); } + ~PatchSizeRestore() + { + gGL.setPatchVertices((U32)mFound); + glPatchParameteri(GL_PATCH_VERTICES, mFound); + } + } restore; + + gGL.setPatchVertices(4); + glPatchParameteri(GL_PATCH_VERTICES, 3); + gGL.setPatchVertices(4); + GLint patch = 0; + glGetIntegerv(GL_PATCH_VERTICES, &patch); + UNSCOPED_INFO("an agreeing cache issues nothing"); + CHECK((S32)patch == 3); + + gGL.refreshState(); + glGetIntegerv(GL_PATCH_VERTICES, &patch); + UNSCOPED_INFO("refreshState restores the patch size"); + CHECK((S32)patch == 4); + UNSCOPED_INFO("no GL error"); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } } diff --git a/indra/llrender/tests/altessellation_gl_test.cpp b/indra/llrender/tests/altessellation_gl_test.cpp index 7e8073e894..74978e9487 100644 --- a/indra/llrender/tests/altessellation_gl_test.cpp +++ b/indra/llrender/tests/altessellation_gl_test.cpp @@ -34,16 +34,14 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include #include #include -namespace tut +namespace { - namespace - { // The stages the terrain program has: a pass-through vertex shader, a // control shader that takes its levels from uniforms, an evaluation // shader that bilinearly places the quad, and a flat fragment shader. @@ -273,56 +271,63 @@ namespace tut U32 mPriorArrays = 0; }; - typedef test_group altessellation_test; - typedef altessellation_test::object altessellation_object; - tut::altessellation_test altessellation_testcase("ALTessellation"); - } // The context compiles both tessellation stages and links them with a // vertex and fragment shader. Everything below assumes this. - template<> template<> - void altessellation_object::test<1>() + TEST_CASE_METHOD(altessellation_data, "ALTessellation: the context compiles and links a four-stage tessellation program", "[ALTessellation]") { - ensure("all four stages compiled", mCompiled); - ensure("four-stage program linked", mLinked); - ensure("outer levels uniform found", mOuter >= 0); - ensure("inner levels uniform found", mInner >= 0); + UNSCOPED_INFO("all four stages compiled"); + CHECK(mCompiled); + UNSCOPED_INFO("four-stage program linked"); + CHECK(mLinked); + UNSCOPED_INFO("outer levels uniform found"); + CHECK(mOuter >= 0); + UNSCOPED_INFO("inner levels uniform found"); + CHECK(mInner >= 0); } // At level 1 everywhere a counter-clockwise patch is two triangles over // the whole framebuffer, and back-face culling keeps them. - template<> template<> - void altessellation_object::test<2>() + TEST_CASE_METHOD(altessellation_data, "ALTessellation: at level 1 a counter-clockwise patch covers the whole framebuffer", "[ALTessellation]") { - ensure("program linked", mLinked); + UNSCOPED_INFO("program linked"); + CHECK(mLinked); setLevels(1, 1, 1, 1, 1, 1); const std::vector px = draw(kCCW); - ensure("bottom-left covered", white(px, 1, 1)); - ensure("bottom-right covered", white(px, W - 2, 1)); - ensure("top-right covered", white(px, W - 2, H - 2)); - ensure("top-left covered", white(px, 1, H - 2)); - ensure("centre covered", white(px, W / 2, H / 2)); + UNSCOPED_INFO("bottom-left covered"); + CHECK(white(px, 1, 1)); + UNSCOPED_INFO("bottom-right covered"); + CHECK(white(px, W - 2, 1)); + UNSCOPED_INFO("top-right covered"); + CHECK(white(px, W - 2, H - 2)); + UNSCOPED_INFO("top-left covered"); + CHECK(white(px, 1, H - 2)); + UNSCOPED_INFO("centre covered"); + CHECK(white(px, W / 2, H / 2)); } // The same corners clockwise are back faces: `layout(ccw)` with u along // x and v along y is the winding the pipeline's cull expects. - template<> template<> - void altessellation_object::test<3>() + TEST_CASE_METHOD(altessellation_data, "ALTessellation: the same corners clockwise are culled as back faces", "[ALTessellation]") { - ensure("program linked", mLinked); + UNSCOPED_INFO("program linked"); + CHECK(mLinked); setLevels(1, 1, 1, 1, 1, 1); const std::vector px = draw(kCW); - ensure("bottom-left culled", black(px, 1, 1)); - ensure("top-right culled", black(px, W - 2, H - 2)); - ensure("centre culled", black(px, W / 2, H / 2)); + UNSCOPED_INFO("bottom-left culled"); + CHECK(black(px, 1, 1)); + UNSCOPED_INFO("top-right culled"); + CHECK(black(px, W - 2, H - 2)); + UNSCOPED_INFO("centre culled"); + CHECK(black(px, W / 2, H / 2)); } // Subdivided with fractional spacing the patch still covers every pixel: // no cracks between the pieces, at integer and at fractional levels. - template<> template<> - void altessellation_object::test<4>() + TEST_CASE_METHOD(altessellation_data, "ALTessellation: subdivided with fractional spacing the patch still covers every pixel", "[ALTessellation]") { - ensure("program linked", mLinked); + UNSCOPED_INFO("program linked"); + CHECK(mLinked); for (const F32 level : { 4.f, 2.5f, 7.25f }) { setLevels(level, level, level, level, level, level); @@ -331,7 +336,8 @@ namespace tut for (S32 y = 0; y < H; ++y) for (S32 x = 0; x < W; ++x) if (!white(px, x, y)) ++uncovered; - ensure_equals("every pixel covered at level " + std::to_string(level), uncovered, size_t(0)); + UNSCOPED_INFO("every pixel covered at level " + std::to_string(level)); + REQUIRE(uncovered == size_t(0)); } } @@ -339,27 +345,29 @@ namespace tut // honours the levels and transform feedback sees what it emits. Odd // spacing rounds a level up to the next odd integer, so 3 is the smallest // level above 1 whose segments are all equal and whose count is exact. - template<> template<> - void altessellation_object::test<5>() + TEST_CASE_METHOD(altessellation_data, "ALTessellation: the tessellator emits two triangles at level 1 and eighteen at level 3", "[ALTessellation]") { - ensure("program linked", mLinked); + UNSCOPED_INFO("program linked"); + CHECK(mLinked); GLuint triangles = 0; setLevels(1, 1, 1, 1, 1, 1); capture(kCCW, triangles); - ensure_equals("level 1 is two triangles", triangles, GLuint(2)); + UNSCOPED_INFO("level 1 is two triangles"); + CHECK(triangles == GLuint(2)); setLevels(3, 3, 3, 3, 3, 3); capture(kCCW, triangles); - ensure_equals("level 3 is eighteen triangles", triangles, GLuint(18)); + UNSCOPED_INFO("level 3 is eighteen triangles"); + CHECK(triangles == GLuint(18)); } // Which outer level drives which edge. Raising one outer level to 3 puts // four distinct vertices on exactly one side of the patch and leaves two // on every other. Outer[0] is the u = 0 edge (x = -1), [1] the v = 0 edge // (y = -1), [2] u = 1 (x = +1), [3] v = 1 (y = +1). - template<> template<> - void altessellation_object::test<6>() + TEST_CASE_METHOD(altessellation_data, "ALTessellation: each outer level splits only its own edge of the patch", "[ALTessellation]") { - ensure("program linked", mLinked); + UNSCOPED_INFO("program linked"); + CHECK(mLinked); // Edge i of the patch, as outer[i] should see it. struct Edge { int axis; F32 value; const char* name; }; const Edge edges[4] = { @@ -378,25 +386,26 @@ namespace tut for (int i = 0; i < 4; ++i) { const size_t expected = i == raised ? 4 : 2; - ensure_equals(std::string(edges[raised].name) + (i == raised ? "" : ": other side unsplit"), - distinctOn(verts, edges[i].axis, edges[i].value), expected); + UNSCOPED_INFO(std::string(edges[raised].name) + (i == raised ? "" : ": other side unsplit")); + REQUIRE(distinctOn(verts, edges[i].axis, edges[i].value) == expected); } } } // Profiling a tessellated draw must still render. Apple's Metal-backed // OpenGL driver aborts when GL_PRIMITIVES_GENERATED is active here. - template<> template<> - void altessellation_object::test<7>() + TEST_CASE_METHOD(altessellation_data, "ALTessellation: a profiled tessellated draw still renders", "[ALTessellation]") { - ensure("program linked", mLinked); + UNSCOPED_INFO("program linked"); + CHECK(mLinked); // Viewer programs do not capture transform feedback. Declaring a // captured varying takes a different Apple driver path and hides the crash. glTransformFeedbackVaryings(mProgram, 0, nullptr, GL_INTERLEAVED_ATTRIBS); glLinkProgram(mProgram); GLint linked = GL_FALSE; glGetProgramiv(mProgram, GL_LINK_STATUS, &linked); - ensure("program linked without transform feedback", linked == GL_TRUE); + UNSCOPED_INFO("program linked without transform feedback"); + CHECK(linked == GL_TRUE); mOuter = glGetUniformLocation(mProgram, "outer"); mInner = glGetUniformLocation(mProgram, "inner"); setLevels(3, 3, 3, 3, 3, 3); @@ -410,9 +419,13 @@ namespace tut LLGLSLShader::sProfileEnabled = false; const GLuint queries[] = { profile.mTimerQuery, profile.mSamplesQuery, profile.mPrimitivesQuery }; glDeleteQueries(3, queries); - ensure("profile read completed", read); - ensure("profiled terrain covers centre", white(px, W / 2, H / 2)); - ensure("sample query still counts fragments", profile.mSamplesDrawn > 0); - ensure_equals("profiling leaves no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("profile read completed"); + CHECK(read); + UNSCOPED_INFO("profiled terrain covers centre"); + CHECK(white(px, W / 2, H / 2)); + UNSCOPED_INFO("sample query still counts fragments"); + CHECK(profile.mSamplesDrawn > 0); + UNSCOPED_INFO("profiling leaves no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } } diff --git a/indra/llrender/tests/alwindowsdlheadless_test.cpp b/indra/llrender/tests/alwindowsdlheadless_test.cpp index 145ec90e06..5a07de583a 100644 --- a/indra/llrender/tests/alwindowsdlheadless_test.cpp +++ b/indra/llrender/tests/alwindowsdlheadless_test.cpp @@ -31,12 +31,10 @@ #include "llwindow.h" -#include "../test/lltut.h" +#include #include -namespace tut -{ namespace { struct alwindowsdlheadless_data @@ -53,41 +51,41 @@ namespace alwindowsdlheadless_data() { gl(); } }; - typedef test_group alwindowsdlheadless_test; - typedef alwindowsdlheadless_test::object alwindowsdlheadless_object; - tut::alwindowsdlheadless_test alwindowsdlheadless_testcase("ALWindowSDLHeadless"); -} // The window the manager handed the fixture is the hidden backend, valid, // with an SDL window behind it and the size that was asked for. - template<> template<> - void alwindowsdlheadless_object::test<1>() + TEST_CASE_METHOD(alwindowsdlheadless_data, "ALWindowSDLHeadless: the fixture's window is the hidden backend at the size asked for", "[ALWindowSDLHeadless]") { LLWindow* window = gl().window(); - ensure("a window", window != nullptr); - ensure("the hidden backend was asked for", - LLWindowManager::getBackend() == ALWindowBackend::Hidden); + UNSCOPED_INFO("a window"); + REQUIRE(window != nullptr); + UNSCOPED_INFO("the hidden backend was asked for"); + CHECK(LLWindowManager::getBackend() == ALWindowBackend::Hidden); // createWindow would have returned null on an invalid one; what is // left to see is the SDL window behind it. - ensure("there is an SDL window behind it", window->getPlatformWindow() != nullptr); + UNSCOPED_INFO("there is an SDL window behind it"); + REQUIRE(window->getPlatformWindow() != nullptr); LLCoordWindow size; - ensure("the size can be read", window->getSize(&size)); - ensure_equals("width as asked", size.mX, ll_test::HeadlessGL::WIDTH); - ensure_equals("height as asked", size.mY, ll_test::HeadlessGL::HEIGHT); + UNSCOPED_INFO("the size can be read"); + REQUIRE(window->getSize(&size)); + UNSCOPED_INFO("width as asked"); + CHECK(size.mX == ll_test::HeadlessGL::WIDTH); + UNSCOPED_INFO("height as asked"); + CHECK(size.mY == ll_test::HeadlessGL::HEIGHT); } // A worker thread's shared context, as LLImageGLThread and the VBO // streaming thread use it: made on the main thread, bound and released // on the worker, and the texture the worker makes on it is the same // object on the main thread's context afterwards. - template<> template<> - void alwindowsdlheadless_object::test<2>() + TEST_CASE_METHOD(alwindowsdlheadless_data, "ALWindowSDLHeadless: a texture made on a worker's shared context is the main one's too", "[ALWindowSDLHeadless]") { LLWindow* window = gl().window(); void* shared = window->createSharedContext(); - ensure("a shared context was made", shared != nullptr); + UNSCOPED_INFO("a shared context was made"); + REQUIRE(shared != nullptr); GLuint name = 0; GLenum worker_error = GL_NO_ERROR; @@ -109,9 +107,12 @@ namespace }); worker.join(); - ensure_equals("the worker's GL calls raised no error", worker_error, (GLenum)GL_NO_ERROR); - ensure("the worker made a texture", name != 0); - ensure("the main context sees the worker's texture", glIsTexture(name) == GL_TRUE); + UNSCOPED_INFO("the worker's GL calls raised no error"); + CHECK(worker_error == (GLenum)GL_NO_ERROR); + UNSCOPED_INFO("the worker made a texture"); + CHECK(name != 0); + UNSCOPED_INFO("the main context sees the worker's texture"); + CHECK(glIsTexture(name) == GL_TRUE); // The shared context's texture slots cache what they bound, so the // read puts back the texture it found. @@ -121,20 +122,23 @@ namespace GLint width = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width); glBindTexture(GL_TEXTURE_2D, prior); - ensure_equals("with the storage the worker gave it", width, 1); - ensure_equals("and no error on this side", glGetError(), (GLenum)GL_NO_ERROR); + UNSCOPED_INFO("with the storage the worker gave it"); + CHECK(width == 1); + UNSCOPED_INFO("and no error on this side"); + CHECK(glGetError() == (GLenum)GL_NO_ERROR); glDeleteTextures(1, &name); } // A shared context nobody bound is still a shared context to destroy. - template<> template<> - void alwindowsdlheadless_object::test<3>() + TEST_CASE_METHOD(alwindowsdlheadless_data, "ALWindowSDLHeadless: a shared context nobody bound can still be destroyed", "[ALWindowSDLHeadless]") { LLWindow* window = gl().window(); void* shared = window->createSharedContext(); - ensure("a shared context was made", shared != nullptr); + UNSCOPED_INFO("a shared context was made"); + REQUIRE(shared != nullptr); window->destroySharedContext(shared); - ensure_equals("the main context is untouched", glGetError(), (GLenum)GL_NO_ERROR); + UNSCOPED_INFO("the main context is untouched"); + CHECK(glGetError() == (GLenum)GL_NO_ERROR); } } diff --git a/indra/llrender/tests/llfontbitmapcache_gl_test.cpp b/indra/llrender/tests/llfontbitmapcache_gl_test.cpp index de3ca03b41..3302f5b82c 100644 --- a/indra/llrender/tests/llfontbitmapcache_gl_test.cpp +++ b/indra/llrender/tests/llfontbitmapcache_gl_test.cpp @@ -31,10 +31,8 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include -namespace tut -{ namespace { using namespace ll_test; @@ -48,133 +46,139 @@ namespace std::unique_ptr gl = std::make_unique(); }; - typedef test_group llfontbitmapcache_gl_test; - typedef llfontbitmapcache_gl_test::object llfontbitmapcache_gl_object; - tut::llfontbitmapcache_gl_test llfontbitmapcache_gl_testcase("LLFontBitmapCacheGL"); -} // After a Grayscale allocation, the underlying LLImageRaw must // carry 2 components (R+G with R=255 stem, G=alpha) — pins // c99dcaa7d3 which restored the greyscale atlas to 2 components // after a regression had it at 1. - template<> template<> - void llfontbitmapcache_gl_object::test<1>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: a Grayscale sheet has 2 components", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); S32 px = 0, py = 0; U32 sheet = 0; - ensure("nextOpenPos succeeded", - c.nextOpenPos(/*width=*/8, /*height=*/8, px, py, EFontGlyphType::Grayscale, sheet)); + UNSCOPED_INFO("nextOpenPos succeeded"); + CHECK(c.nextOpenPos(/*width=*/8, /*height=*/8, px, py, EFontGlyphType::Grayscale, sheet)); LLImageRaw* raw = c.getImageRaw(EFontGlyphType::Grayscale, sheet); - ensure("raw image present", raw != nullptr); - ensure_equals("Grayscale atlas has 2 components", - (S32)raw->getComponents(), 2); + UNSCOPED_INFO("raw image present"); + REQUIRE(raw != nullptr); + UNSCOPED_INFO("Grayscale atlas has 2 components"); + CHECK((S32)raw->getComponents() == 2); } // Color allocation must produce a 4-component (RGBA) raw — pins // the same array-init bug from c99dcaa7d3 in its Color form. - template<> template<> - void llfontbitmapcache_gl_object::test<2>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: a Color sheet has 4 components", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); S32 px = 0, py = 0; U32 sheet = 0; - ensure("nextOpenPos succeeded", - c.nextOpenPos(8, 8, px, py, EFontGlyphType::Color, sheet)); + UNSCOPED_INFO("nextOpenPos succeeded"); + CHECK(c.nextOpenPos(8, 8, px, py, EFontGlyphType::Color, sheet)); LLImageRaw* raw = c.getImageRaw(EFontGlyphType::Color, sheet); - ensure("raw image present", raw != nullptr); - ensure_equals("Color atlas has 4 components", - (S32)raw->getComponents(), 4); + UNSCOPED_INFO("raw image present"); + REQUIRE(raw != nullptr); + UNSCOPED_INFO("Color atlas has 4 components"); + CHECK((S32)raw->getComponents() == 4); } // The very first nextOpenPos on a fresh cache must yield (4, 4) — // pins the mCurrentOffsetX/Y = {4, 4} array-init in the header // (an earlier `= { 4 }` form was zero-initing index [1], which // started Color atlases at offset 0). - template<> template<> - void llfontbitmapcache_gl_object::test<3>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: the first Grayscale and Color allocations start past the 4px border", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); S32 px = -1, py = -1; U32 sheet = 99; - ensure("first Grayscale alloc succeeded", - c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, sheet)); - ensure_equals("first Grayscale posX == 4", px, 4); - ensure_equals("first Grayscale posY == 4", py, 4); - ensure_equals("first Grayscale sheet index == 0", (S32)sheet, 0); + UNSCOPED_INFO("first Grayscale alloc succeeded"); + CHECK(c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, sheet)); + UNSCOPED_INFO("first Grayscale posX == 4"); + CHECK(px == 4); + UNSCOPED_INFO("first Grayscale posY == 4"); + CHECK(py == 4); + UNSCOPED_INFO("first Grayscale sheet index == 0"); + CHECK((S32)sheet == 0); // Repeat for Color — a fresh cache, fresh allocation. The bug // would have started Color at posX=0; the fix shows posX=4. LLFontBitmapCache c2; c2.init(2, 2); S32 px2 = -1, py2 = -1; U32 sheet2 = 99; - ensure("first Color alloc succeeded", - c2.nextOpenPos(8, 8, px2, py2, EFontGlyphType::Color, sheet2)); - ensure_equals("first Color posX == 4", px2, 4); - ensure_equals("first Color posY == 4", py2, 4); + UNSCOPED_INFO("first Color alloc succeeded"); + CHECK(c2.nextOpenPos(8, 8, px2, py2, EFontGlyphType::Color, sheet2)); + UNSCOPED_INFO("first Color posX == 4"); + CHECK(px2 == 4); + UNSCOPED_INFO("first Color posY == 4"); + CHECK(py2 == 4); } // Sequential allocations within the same row advance posX by // width + 4 (4-pixel border). Tests the row-packing pen advance // at llfontbitmapcache.cpp:201. - template<> template<> - void llfontbitmapcache_gl_object::test<4>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: allocations in a row advance x by the width and the 4px border", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); S32 px = 0, py = 0; U32 sheet = 0; c.nextOpenPos(10, 8, px, py, EFontGlyphType::Grayscale, sheet); - ensure_equals("first posX", px, 4); - ensure_equals("first posY", py, 4); + UNSCOPED_INFO("first posX"); + CHECK(px == 4); + UNSCOPED_INFO("first posY"); + CHECK(py == 4); c.nextOpenPos(10, 8, px, py, EFontGlyphType::Grayscale, sheet); // Expected: 4 (initial border) + 10 (first width) + 4 (border) - ensure_equals("second posX advances by width+4", px, 18); - ensure_equals("second posY stays in same row", py, 4); + UNSCOPED_INFO("second posX advances by width+4"); + CHECK(px == 18); + UNSCOPED_INFO("second posY stays in same row"); + CHECK(py == 4); } // When the next allocation would exceed mBitmapWidth, the row // wraps. posX resets to 4; posY advances by the tallest glyph // placed in the prior row (mCurrentRowMaxHeight, not mMaxCharHeight). - template<> template<> - void llfontbitmapcache_gl_object::test<5>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: a full row wraps below the tallest glyph placed in it", "[LLFontBitmapCacheGL]") { // 128x128 atlas. Two 50px-wide glyphs fit in row 0 // (4 + 50 + 4 + 50 + 4 = 112 ≤ 128); third forces wrap. LLFontBitmapCache c; c.init(2, 2); - ensure_equals("sheet width is 128", c.getBitmapWidth(), 128); + UNSCOPED_INFO("sheet width is 128"); + CHECK(c.getBitmapWidth() == 128); S32 px = 0, py = 0; U32 sheet = 0; c.nextOpenPos(50, 30, px, py, EFontGlyphType::Grayscale, sheet); // row 0, col 0 c.nextOpenPos(50, 20, px, py, EFontGlyphType::Grayscale, sheet); // row 0, col 1 - ensure_equals("second alloc still in row 0", py, 4); + UNSCOPED_INFO("second alloc still in row 0"); + CHECK(py == 4); c.nextOpenPos(50, 8, px, py, EFontGlyphType::Grayscale, sheet); // wraps to row 1 - ensure_equals("third alloc wraps posX to 4", px, 4); + UNSCOPED_INFO("third alloc wraps posX to 4"); + CHECK(px == 4); // Tallest in row 0 was 30, so row 1 starts at 4 + 30 + 4 = 38. - ensure_equals("third alloc posY uses tallest glyph in prior row", - py, 38); - ensure_equals("still on sheet 0 (row wrap, not sheet rollover)", - (S32)sheet, 0); + UNSCOPED_INFO("third alloc posY uses tallest glyph in prior row"); + CHECK(py == 38); + UNSCOPED_INFO("still on sheet 0 (row wrap, not sheet rollover)"); + CHECK((S32)sheet == 0); } // When a row wrap would push posY past mBitmapHeight, the cache // must allocate a fresh sheet rather than write past the atlas. // Verify by repeatedly forcing wraps until the next would overflow. - template<> template<> - void llfontbitmapcache_gl_object::test<6>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: a row wrap that would pass the bottom starts a new sheet", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); - ensure_equals("sheet width is 128", c.getBitmapWidth(), 128); - ensure_equals("sheet height is 128", c.getBitmapHeight(), 128); + UNSCOPED_INFO("sheet width is 128"); + CHECK(c.getBitmapWidth() == 128); + UNSCOPED_INFO("sheet height is 128"); + CHECK(c.getBitmapHeight() == 128); // Glyphs 50 wide, 30 tall. 2 per row (positions 4 and 58), // 3 rows (Y = 4, 38, 72) before next would land at Y=106 @@ -184,28 +188,29 @@ namespace { c.nextOpenPos(50, 30, px, py, EFontGlyphType::Grayscale, sheet); } - ensure_equals("six allocs of 50x30 still on sheet 0", - (S32)sheet, 0); - ensure_equals("one sheet so far", - c.getNumBitmaps(EFontGlyphType::Grayscale), 1u); + UNSCOPED_INFO("six allocs of 50x30 still on sheet 0"); + CHECK((S32)sheet == 0); + UNSCOPED_INFO("one sheet so far"); + CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); // 7th glyph: Y advance to 4+30+4+30+4+30+4=106, bottom would // be 106+30+4=140 > 128 → new sheet. Either the row-wrap path // (line 125) or the vertical-fit recheck (line 144) fires. c.nextOpenPos(50, 30, px, py, EFontGlyphType::Grayscale, sheet); - ensure_equals("7th alloc moved to new sheet", - (S32)sheet, 1); - ensure_equals("two sheets allocated", - c.getNumBitmaps(EFontGlyphType::Grayscale), 2u); - ensure_equals("new sheet starts at posX=4", px, 4); - ensure_equals("new sheet starts at posY=4", py, 4); + UNSCOPED_INFO("7th alloc moved to new sheet"); + CHECK((S32)sheet == 1); + UNSCOPED_INFO("two sheets allocated"); + CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 2u); + UNSCOPED_INFO("new sheet starts at posX=4"); + CHECK(px == 4); + UNSCOPED_INFO("new sheet starts at posY=4"); + CHECK(py == 4); } // Per-type isolation: a Grayscale allocation must not move the // Color pen, and vice versa. Each type owns its own X/Y/row-max // state at index [bitmap_idx]. - template<> template<> - void llfontbitmapcache_gl_object::test<7>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: Grayscale and Color pens and sheets are independent", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); @@ -217,21 +222,23 @@ namespace // Color must still be at the (4, 4) start. S32 cpx = 0, cpy = 0; U32 csheet = 99; c.nextOpenPos(8, 8, cpx, cpy, EFontGlyphType::Color, csheet); - ensure_equals("Color pen unaffected by Grayscale activity (x)", cpx, 4); - ensure_equals("Color pen unaffected by Grayscale activity (y)", cpy, 4); - ensure_equals("Color sheet index starts at 0", (S32)csheet, 0); - ensure_equals("Grayscale + Color each have one sheet", - c.getNumBitmaps(EFontGlyphType::Grayscale), 1u); - ensure_equals("Color has its own sheet vector", - c.getNumBitmaps(EFontGlyphType::Color), 1u); + UNSCOPED_INFO("Color pen unaffected by Grayscale activity (x)"); + CHECK(cpx == 4); + UNSCOPED_INFO("Color pen unaffected by Grayscale activity (y)"); + CHECK(cpy == 4); + UNSCOPED_INFO("Color sheet index starts at 0"); + CHECK((S32)csheet == 0); + UNSCOPED_INFO("Grayscale + Color each have one sheet"); + CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); + UNSCOPED_INFO("Color has its own sheet vector"); + CHECK(c.getNumBitmaps(EFontGlyphType::Color) == 1u); } // Generation counter must strictly increase on every allocation // and on releaseSheet. Vertex buffers compare against a captured // generation to invalidate; equal-or-decreasing generations would // mask the invalidation. - template<> template<> - void llfontbitmapcache_gl_object::test<8>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: the generation rises on allocation and release and reset", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); @@ -240,30 +247,32 @@ namespace S32 px = 0, py = 0; U32 sheet = 0; c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, sheet); const S32 g1 = c.getCacheGeneration(); - ensure("generation bumped by nextOpenPos", g1 > g0); + UNSCOPED_INFO("generation bumped by nextOpenPos"); + CHECK(g1 > g0); c.releaseSheet(EFontGlyphType::Grayscale, sheet); const S32 g2 = c.getCacheGeneration(); - ensure("generation bumped by releaseSheet", g2 > g1); + UNSCOPED_INFO("generation bumped by releaseSheet"); + CHECK(g2 > g1); c.reset(); const S32 g3 = c.getCacheGeneration(); - ensure("generation bumped by reset", g3 > g2); + UNSCOPED_INFO("generation bumped by reset"); + CHECK(g3 > g2); } // releaseSheet drops the LLImageRaw + LLImageGL but keeps the // slot index alive (as nullptr) so existing index references stay // valid. isSheetReleased flips true; getSheetLastUsedTime returns 0. - template<> template<> - void llfontbitmapcache_gl_object::test<9>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: releaseSheet frees the images and keeps the slot", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); S32 px = 0, py = 0; U32 sheet = 0; c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, sheet); - ensure("sheet not yet released", - !c.isSheetReleased(EFontGlyphType::Grayscale, sheet)); + UNSCOPED_INFO("sheet not yet released"); + CHECK_FALSE(c.isSheetReleased(EFontGlyphType::Grayscale, sheet)); // Don't assert getSheetLastUsedTime is > 0 here — it samples // LLFrameTimer::getTotalSeconds() and the test environment may // not have ticked a frame yet, so the wall-clock can legitimately @@ -273,15 +282,15 @@ namespace c.releaseSheet(EFontGlyphType::Grayscale, sheet); - ensure("isSheetReleased flips true after release", - c.isSheetReleased(EFontGlyphType::Grayscale, sheet)); - ensure_equals("released sheet getImageRaw returns null", - c.getImageRaw(EFontGlyphType::Grayscale, sheet), + UNSCOPED_INFO("isSheetReleased flips true after release"); + CHECK(c.isSheetReleased(EFontGlyphType::Grayscale, sheet)); + UNSCOPED_INFO("released sheet getImageRaw returns null"); + REQUIRE(c.getImageRaw(EFontGlyphType::Grayscale, sheet) == (LLImageRaw*)nullptr); - ensure_equals("released sheet last-used time cleared to 0", - c.getSheetLastUsedTime(EFontGlyphType::Grayscale, sheet), 0.0); - ensure_equals("getNumBitmaps preserves slot count after release", - c.getNumBitmaps(EFontGlyphType::Grayscale), 1u); + UNSCOPED_INFO("released sheet last-used time cleared to 0"); + CHECK(c.getSheetLastUsedTime(EFontGlyphType::Grayscale, sheet) == 0.0); + UNSCOPED_INFO("getNumBitmaps preserves slot count after release"); + CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); } // After releasing the active sheet, the next nextOpenPos must build a @@ -291,8 +300,7 @@ namespace // index, so reuse is safe and bounds slot growth across eviction // cycles. Pins the active_sheet_released check + slot recycling in // nextOpenPos. - template<> template<> - void llfontbitmapcache_gl_object::test<10>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: an allocation after the active sheet's release rebuilds it in that slot", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); @@ -301,16 +309,47 @@ namespace c.releaseSheet(EFontGlyphType::Grayscale, sheet); S32 px2 = 0, py2 = 0; U32 sheet2 = 99; - ensure("nextOpenPos succeeds after active sheet release", - c.nextOpenPos(8, 8, px2, py2, EFontGlyphType::Grayscale, sheet2)); - ensure_equals("released slot 0 is recycled for the new sheet", - (S32)sheet2, 0); - ensure_equals("new sheet starts at posX=4", px2, 4); - ensure_equals("new sheet starts at posY=4", py2, 4); - ensure("recycled slot 0 is live again", - !c.isSheetReleased(EFontGlyphType::Grayscale, 0)); - ensure_equals("slot vector did not grow", - c.getNumBitmaps(EFontGlyphType::Grayscale), 1u); + UNSCOPED_INFO("nextOpenPos succeeds after active sheet release"); + CHECK(c.nextOpenPos(8, 8, px2, py2, EFontGlyphType::Grayscale, sheet2)); + UNSCOPED_INFO("released slot 0 is recycled for the new sheet"); + CHECK((S32)sheet2 == 0); + UNSCOPED_INFO("new sheet starts at posX=4"); + CHECK(px2 == 4); + UNSCOPED_INFO("new sheet starts at posY=4"); + CHECK(py2 == 4); + UNSCOPED_INFO("recycled slot 0 is live again"); + CHECK_FALSE(c.isSheetReleased(EFontGlyphType::Grayscale, 0)); + UNSCOPED_INFO("slot vector did not grow"); + CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); + } + + // reset() after allocations clears all state — no live sheets, + // pens back at 4, and a subsequent nextOpenPos starts from (4, 4) + // on a fresh sheet. + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: reset clears every sheet and the pen starts over", "[LLFontBitmapCacheGL]") + { + LLFontBitmapCache c; + c.init(2, 2); + S32 px = 0, py = 0; U32 sheet = 0; + c.nextOpenPos(40, 20, px, py, EFontGlyphType::Grayscale, sheet); + c.nextOpenPos(40, 20, px, py, EFontGlyphType::Grayscale, sheet); + UNSCOPED_INFO("two sheets pending before reset"); + CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); + + c.reset(); + UNSCOPED_INFO("no Grayscale sheets after reset"); + CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 0u); + UNSCOPED_INFO("no Color sheets after reset"); + CHECK(c.getNumBitmaps(EFontGlyphType::Color) == 0u); + + // Re-init and verify pens really did reset back to (4,4). + c.init(2, 2); + S32 px2 = -1, py2 = -1; U32 sheet2 = 99; + c.nextOpenPos(8, 8, px2, py2, EFontGlyphType::Grayscale, sheet2); + UNSCOPED_INFO("post-reset first posX"); + CHECK(px2 == 4); + UNSCOPED_INFO("post-reset first posY"); + CHECK(py2 == 4); } // Multi-cycle release+re-alloc: alloc, release, alloc, release, alloc @@ -318,40 +357,43 @@ namespace // cycles, and the generation counter still advances every cycle so // captured vertex buffers rebuild. Pins bounded slot growth under // repeat cycles. - template<> template<> - void llfontbitmapcache_gl_object::test<12>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: repeated release and allocation keeps recycling slot 0", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); S32 px = 0, py = 0; U32 s0 = 0, s1 = 99, s2 = 99; c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, s0); - ensure_equals("first alloc lands on sheet 0", (S32)s0, 0); + UNSCOPED_INFO("first alloc lands on sheet 0"); + CHECK((S32)s0 == 0); const S32 gen0 = c.getCacheGeneration(); c.releaseSheet(EFontGlyphType::Grayscale, s0); c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, s1); - ensure_equals("second alloc recycles slot 0", (S32)s1, 0); + UNSCOPED_INFO("second alloc recycles slot 0"); + CHECK((S32)s1 == 0); const S32 gen1 = c.getCacheGeneration(); - ensure("generation advanced across release+recycle", gen1 > gen0); + UNSCOPED_INFO("generation advanced across release+recycle"); + CHECK(gen1 > gen0); c.releaseSheet(EFontGlyphType::Grayscale, s1); c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, s2); - ensure_equals("third alloc recycles slot 0 again", (S32)s2, 0); - ensure("generation advanced again", c.getCacheGeneration() > gen1); - - ensure_equals("getNumBitmaps == 1 (slots recycled, not reserved)", - c.getNumBitmaps(EFontGlyphType::Grayscale), 1u); - ensure("slot 0 live after the final alloc", - !c.isSheetReleased(EFontGlyphType::Grayscale, 0)); + UNSCOPED_INFO("third alloc recycles slot 0 again"); + CHECK((S32)s2 == 0); + UNSCOPED_INFO("generation advanced again"); + CHECK(c.getCacheGeneration() > gen1); + + UNSCOPED_INFO("getNumBitmaps == 1 (slots recycled, not reserved)"); + CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); + UNSCOPED_INFO("slot 0 live after the final alloc"); + CHECK_FALSE(c.isSheetReleased(EFontGlyphType::Grayscale, 0)); } // A live active sheet with room keeps absorbing allocations — a // release+recycle cycle doesn't perturb the pen, and no spurious // sheets are created while the active sheet has space. Pins the // active-sheet tracking (mCurrentSheet) staying put across allocs. - template<> template<> - void llfontbitmapcache_gl_object::test<13>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: a recycled active sheet keeps taking allocations while it has room", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); @@ -362,7 +404,8 @@ namespace c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, s0); c.releaseSheet(EFontGlyphType::Grayscale, s0); c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, s1); - ensure_equals("after release+alloc, recycled onto slot 0", (S32)s1, 0); + UNSCOPED_INFO("after release+alloc, recycled onto slot 0"); + CHECK((S32)s1 == 0); // Further allocs must keep landing on the recycled sheet (it has // plenty of room — only one 8x8 glyph placed since the rebuild). @@ -370,13 +413,13 @@ namespace { U32 si = 99; c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, si); - ensure_equals("subsequent alloc stays on the recycled sheet", - (S32)si, 0); + UNSCOPED_INFO("subsequent alloc stays on the recycled sheet"); + REQUIRE((S32)si == 0); } - ensure_equals("still 1 sheet slot (no extra allocation)", - c.getNumBitmaps(EFontGlyphType::Grayscale), 1u); - ensure("recycled sheet stays live", - !c.isSheetReleased(EFontGlyphType::Grayscale, 0)); + UNSCOPED_INFO("still 1 sheet slot (no extra allocation)"); + CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); + UNSCOPED_INFO("recycled sheet stays live"); + CHECK_FALSE(c.isSheetReleased(EFontGlyphType::Grayscale, 0)); } // Cross-instance global generation: two LLFontBitmapCache instances @@ -384,8 +427,7 @@ namespace // that vertex buffers checking `mLastFontCacheGen` against // getGlobalGeneration() can detect changes that fired on any cache, // not just the one they captured against. - template<> template<> - void llfontbitmapcache_gl_object::test<14>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: two caches' allocations and releases each bump the global generation", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c1; c1.init(2, 2); LLFontBitmapCache c2; c2.init(2, 2); @@ -394,19 +436,23 @@ namespace const S32 g0 = LLFontBitmapCache::getGlobalGeneration(); c1.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, sheet); const S32 g1 = LLFontBitmapCache::getGlobalGeneration(); - ensure("c1 alloc bumped global gen", g1 > g0); + UNSCOPED_INFO("c1 alloc bumped global gen"); + CHECK(g1 > g0); c2.nextOpenPos(8, 8, px, py, EFontGlyphType::Color, sheet); const S32 g2 = LLFontBitmapCache::getGlobalGeneration(); - ensure("c2 alloc bumped global gen", g2 > g1); + UNSCOPED_INFO("c2 alloc bumped global gen"); + CHECK(g2 > g1); c1.releaseSheet(EFontGlyphType::Grayscale, 0); const S32 g3 = LLFontBitmapCache::getGlobalGeneration(); - ensure("c1 release bumped global gen", g3 > g2); + UNSCOPED_INFO("c1 release bumped global gen"); + CHECK(g3 > g2); c2.releaseSheet(EFontGlyphType::Color, 0); const S32 g4 = LLFontBitmapCache::getGlobalGeneration(); - ensure("c2 release bumped global gen", g4 > g3); + UNSCOPED_INFO("c2 release bumped global gen"); + CHECK(g4 > g3); } // Per-type independence under release: releasing all Color sheets @@ -414,8 +460,7 @@ namespace // queries don't cross-bleed. Pins the per-type vector array layout // — each EFontGlyphType has its own mImageRawVec / pen / released // tracking. - template<> template<> - void llfontbitmapcache_gl_object::test<15>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: releasing the Color sheet leaves Grayscale alone", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); @@ -424,48 +469,22 @@ namespace c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, gs_sheet); c.nextOpenPos(8, 8, px, py, EFontGlyphType::Color, col_sheet); - ensure_equals("Grayscale on its own sheet 0", (S32)gs_sheet, 0); - ensure_equals("Color on its own sheet 0", (S32)col_sheet, 0); + UNSCOPED_INFO("Grayscale on its own sheet 0"); + CHECK((S32)gs_sheet == 0); + UNSCOPED_INFO("Color on its own sheet 0"); + CHECK((S32)col_sheet == 0); c.releaseSheet(EFontGlyphType::Color, col_sheet); - ensure("Color sheet 0 released", - c.isSheetReleased(EFontGlyphType::Color, 0)); - ensure("Grayscale sheet 0 unaffected by Color release", - !c.isSheetReleased(EFontGlyphType::Grayscale, 0)); + UNSCOPED_INFO("Color sheet 0 released"); + CHECK(c.isSheetReleased(EFontGlyphType::Color, 0)); + UNSCOPED_INFO("Grayscale sheet 0 unaffected by Color release"); + CHECK_FALSE(c.isSheetReleased(EFontGlyphType::Grayscale, 0)); // Allocate again on Grayscale: still lands on sheet 0 (its // trailing slot is live, unaffected by Color side). S32 px2 = 0, py2 = 0; U32 gs_sheet2 = 99; c.nextOpenPos(8, 8, px2, py2, EFontGlyphType::Grayscale, gs_sheet2); - ensure_equals("Grayscale alloc stays on sheet 0 (Color release didn't touch it)", - (S32)gs_sheet2, 0); - } - - // reset() after allocations clears all state — no live sheets, - // pens back at 4, and a subsequent nextOpenPos starts from (4, 4) - // on a fresh sheet. - template<> template<> - void llfontbitmapcache_gl_object::test<11>() - { - LLFontBitmapCache c; - c.init(2, 2); - S32 px = 0, py = 0; U32 sheet = 0; - c.nextOpenPos(40, 20, px, py, EFontGlyphType::Grayscale, sheet); - c.nextOpenPos(40, 20, px, py, EFontGlyphType::Grayscale, sheet); - ensure_equals("two sheets pending before reset", - c.getNumBitmaps(EFontGlyphType::Grayscale), 1u); - - c.reset(); - ensure_equals("no Grayscale sheets after reset", - c.getNumBitmaps(EFontGlyphType::Grayscale), 0u); - ensure_equals("no Color sheets after reset", - c.getNumBitmaps(EFontGlyphType::Color), 0u); - - // Re-init and verify pens really did reset back to (4,4). - c.init(2, 2); - S32 px2 = -1, py2 = -1; U32 sheet2 = 99; - c.nextOpenPos(8, 8, px2, py2, EFontGlyphType::Grayscale, sheet2); - ensure_equals("post-reset first posX", px2, 4); - ensure_equals("post-reset first posY", py2, 4); + UNSCOPED_INFO("Grayscale alloc stays on sheet 0 (Color release didn't touch it)"); + CHECK((S32)gs_sheet2 == 0); } } diff --git a/indra/llrender/tests/llfontbitmapcache_test.cpp b/indra/llrender/tests/llfontbitmapcache_test.cpp index 745a211ca9..6a5ded2b13 100644 --- a/indra/llrender/tests/llfontbitmapcache_test.cpp +++ b/indra/llrender/tests/llfontbitmapcache_test.cpp @@ -32,94 +32,81 @@ #include "../llfontbitmapcache.h" -#include "../test/lltut.h" +#include -namespace tut -{ namespace { - // Pure-CPU fixture: no GL context required. The tests in this group - // touch only the bookkeeping state that LLFontBitmapCache maintains - // independently of any GL allocation. - struct llfontbitmapcache_data - { - }; - - typedef test_group llfontbitmapcache_test; - typedef llfontbitmapcache_test::object llfontbitmapcache_object; - tut::llfontbitmapcache_test llfontbitmapcache_testcase("LLFontBitmapCache"); -} - // Construction takes a fresh value from sNextGeneration. Two // back-to-back instances must therefore land on distinct // generations — without per-instance uniqueness, vertex buffers // comparing their stored mLastFontCacheGen across an in-place // font reload would false-match (see llfontbitmapcache.h:139-147). - template<> template<> - void llfontbitmapcache_object::test<1>() + TEST_CASE("LLFontBitmapCache: each new cache takes a fresh and higher generation", "[LLFontBitmapCache]") { LLFontBitmapCache a; LLFontBitmapCache b; - ensure_not_equals("two instances get distinct generations", - a.getCacheGeneration(), b.getCacheGeneration()); - ensure("first generation > 0", a.getCacheGeneration() > 0); - ensure("second generation > first", - b.getCacheGeneration() > a.getCacheGeneration()); + UNSCOPED_INFO("two instances get distinct generations"); + CHECK(a.getCacheGeneration() != b.getCacheGeneration()); + UNSCOPED_INFO("first generation > 0"); + CHECK(a.getCacheGeneration() > 0); + UNSCOPED_INFO("second generation > first"); + CHECK(b.getCacheGeneration() > a.getCacheGeneration()); } // Static getGlobalGeneration must reflect the last sNextGeneration // bump. Constructing a new cache bumps the counter, so the global // tick must equal the new instance's mGeneration immediately after. - template<> template<> - void llfontbitmapcache_object::test<2>() + TEST_CASE("LLFontBitmapCache: getGlobalGeneration is the newest cache's generation", "[LLFontBitmapCache]") { const S32 before = LLFontBitmapCache::getGlobalGeneration(); LLFontBitmapCache c; const S32 after = LLFontBitmapCache::getGlobalGeneration(); - ensure("global generation strictly increased on construction", - after > before); - ensure_equals("global generation matches new instance's gen", - after, c.getCacheGeneration()); + UNSCOPED_INFO("global generation strictly increased on construction"); + CHECK(after > before); + UNSCOPED_INFO("global generation matches new instance's gen"); + CHECK(after == c.getCacheGeneration()); } // init() seeds mBitmapWidth/Height; reset() clears them back to 0 // and bumps the generation counter. Verify both ends. - template<> template<> - void llfontbitmapcache_object::test<3>() + TEST_CASE("LLFontBitmapCache: init sizes the sheets; reset clears them and bumps the generation", "[LLFontBitmapCache]") { LLFontBitmapCache c; const S32 gen_init0 = c.getCacheGeneration(); c.init(/*max_char_width=*/2, /*max_char_height=*/2); // 2*40=80 → next pow2 ≥ 80 → 128, capped at 1024. - ensure_equals("bitmap width set by init", c.getBitmapWidth(), 128); - ensure_equals("bitmap height set by init", c.getBitmapHeight(), 128); + UNSCOPED_INFO("bitmap width set by init"); + CHECK(c.getBitmapWidth() == 128); + UNSCOPED_INFO("bitmap height set by init"); + CHECK(c.getBitmapHeight() == 128); c.reset(); - ensure_equals("bitmap width cleared by reset", c.getBitmapWidth(), 0); - ensure_equals("bitmap height cleared by reset", c.getBitmapHeight(), 0); - ensure("reset bumped generation", - c.getCacheGeneration() > gen_init0); + UNSCOPED_INFO("bitmap width cleared by reset"); + CHECK(c.getBitmapWidth() == 0); + UNSCOPED_INFO("bitmap height cleared by reset"); + CHECK(c.getBitmapHeight() == 0); + UNSCOPED_INFO("reset bumped generation"); + CHECK(c.getCacheGeneration() > gen_init0); } // Out-of-range queries on a freshly-constructed (or reset) cache // must return safe defaults, not crash. EFontGlyphType::Count is // a sentinel — every accessor must reject it. - template<> template<> - void llfontbitmapcache_object::test<4>() + TEST_CASE("LLFontBitmapCache: queries out of range or on an empty cache return safe defaults", "[LLFontBitmapCache]") { LLFontBitmapCache c; - ensure_equals("getNumBitmaps(Count) returns 0", - c.getNumBitmaps(EFontGlyphType::Count), 0u); - ensure("getImageRaw(Count, 0) returns null", - c.getImageRaw(EFontGlyphType::Count, 0) == nullptr); - ensure("getImageGL(Count, 0) returns null", - c.getImageGL(EFontGlyphType::Count, 0) == nullptr); - ensure("getImageRaw on empty Grayscale returns null", - c.getImageRaw(EFontGlyphType::Grayscale, 0) == nullptr); - ensure_equals("getSheetLastUsedTime out-of-range == 0", - c.getSheetLastUsedTime(EFontGlyphType::Grayscale, 0), 0.0); - ensure("isSheetReleased on empty cache reports false (slot doesn't exist)", - !c.isSheetReleased(EFontGlyphType::Grayscale, 0)); + UNSCOPED_INFO("getNumBitmaps(Count) returns 0"); + CHECK(c.getNumBitmaps(EFontGlyphType::Count) == 0u); + UNSCOPED_INFO("getImageRaw(Count, 0) returns null"); + REQUIRE(c.getImageRaw(EFontGlyphType::Count, 0) == nullptr); + UNSCOPED_INFO("getImageGL(Count, 0) returns null"); + REQUIRE(c.getImageGL(EFontGlyphType::Count, 0) == nullptr); + UNSCOPED_INFO("getImageRaw on empty Grayscale returns null"); + REQUIRE(c.getImageRaw(EFontGlyphType::Grayscale, 0) == nullptr); + UNSCOPED_INFO("getSheetLastUsedTime out-of-range == 0"); + CHECK(c.getSheetLastUsedTime(EFontGlyphType::Grayscale, 0) == 0.0); + UNSCOPED_INFO("isSheetReleased on empty cache reports false (slot doesn't exist)"); + CHECK_FALSE(c.isSheetReleased(EFontGlyphType::Grayscale, 0)); } } diff --git a/indra/llrender/tests/llfontfreetype_gl_test.cpp b/indra/llrender/tests/llfontfreetype_gl_test.cpp index 3339f7b510..2292e7fde4 100644 --- a/indra/llrender/tests/llfontfreetype_gl_test.cpp +++ b/indra/llrender/tests/llfontfreetype_gl_test.cpp @@ -34,12 +34,10 @@ #include "llfonttest_helpers.h" #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include -namespace tut -{ namespace { using namespace ll_test; @@ -56,32 +54,32 @@ namespace ll_test::FontStateScope font_scope; }; - typedef test_group llfontfreetype_render_test; - typedef llfontfreetype_render_test::object llfontfreetype_render_object; - tut::llfontfreetype_render_test llfontfreetype_render_testcase("LLFontFreetypeRender"); -} // After getGlyphInfo for an ASCII glyph, the bitmap cache has at // least one Grayscale page, and the returned info has positive // metrics (the FT raster actually ran). - template<> template<> - void llfontfreetype_render_object::test<1>() + TEST_CASE_METHOD(llfontfreetype_render_data, "LLFontFreetypeRender: getGlyphInfo rasterizes an ASCII glyph onto a Grayscale sheet", "[LLFontFreetypeRender]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFtHead(path); - ensure("DejaVuSans loaded as head", ft.notNull()); + UNSCOPED_INFO("DejaVuSans loaded as head"); + REQUIRE(ft.notNull()); LLFontGlyphInfo* gi = ft->getGlyphInfo(L'A', EFontGlyphType::Grayscale); - ensure("getGlyphInfo returned an entry", gi != nullptr); - ensure("glyph has positive width", gi->mWidth > 0); - ensure("glyph has positive advance", gi->mXAdvance > 0.f); + UNSCOPED_INFO("getGlyphInfo returned an entry"); + REQUIRE(gi != nullptr); + UNSCOPED_INFO("glyph has positive width"); + CHECK(gi->mWidth > 0); + UNSCOPED_INFO("glyph has positive advance"); + CHECK(gi->mXAdvance > 0.f); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - ensure("bitmap cache present", cache != nullptr); - ensure("cache has at least one Grayscale sheet", - cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); + UNSCOPED_INFO("bitmap cache present"); + REQUIRE(cache != nullptr); + UNSCOPED_INFO("cache has at least one Grayscale sheet"); + CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); } // collectGarbage is throttled — calling it twice in immediate @@ -91,14 +89,14 @@ namespace // would invalidate sheets if there were anything to evict). // Pins 169390b593 — the cache-sweep gating moved out of the // render hot path. - template<> template<> - void llfontfreetype_render_object::test<2>() + TEST_CASE_METHOD(llfontfreetype_render_data, "LLFontFreetypeRender: two collectGarbage calls in a row return cleanly", "[LLFontFreetypeRender]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFtHead(path); - ensure("DejaVuSans loaded as head", ft.notNull()); + UNSCOPED_INFO("DejaVuSans loaded as head"); + REQUIRE(ft.notNull()); // Force at least one glyph onto the atlas so collectGarbage // has something to consider. (void)ft->getGlyphInfo(L'A', EFontGlyphType::Grayscale); @@ -107,7 +105,8 @@ namespace // doing real work. Both must complete without crashing. ft->collectGarbage(); ft->collectGarbage(); - ensure("collectGarbage throttled call returned cleanly", true); + UNSCOPED_INFO("collectGarbage throttled call returned cleanly"); + CHECK(true); } // LLFontManager::collectGarbage drops face cache entries with @@ -116,40 +115,48 @@ namespace // 'A' holds an entry for it, and a face loaded fresh holds none. The // address of the evicted face is no evidence -- the allocator may hand // the same block straight back to the reload. - template<> template<> - void llfontfreetype_render_object::test<3>() + TEST_CASE_METHOD(llfontfreetype_render_data, "LLFontFreetypeRender: LLFontManager::collectGarbage evicts only faces nothing else holds", "[LLFontFreetypeRender]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); // A head over the face rasterizes 'A' into the face's glyph cache; // loadFtHead's parameters are makeKey's, so the manager hands the // head and this lookup the same face. LLPointer ft = loadFtHead(path); - ensure("DejaVuSans loaded as head", ft.notNull()); + UNSCOPED_INFO("DejaVuSans loaded as head"); + REQUIRE(ft.notNull()); (void)ft->getGlyphInfo(L'A', EFontGlyphType::Grayscale); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("face loaded", face.notNull() && face->isValid()); + { + INFO("face loaded"); + REQUIRE(face.notNull()); + CHECK(face->isValid()); + } const U32 glyph_a = face->getCharGlyphIndex(L'A'); - ensure("the head rasterized into this face", - face->findGlyphInfo(glyph_a, EFontGlyphType::Grayscale) != nullptr); + UNSCOPED_INFO("the head rasterized into this face"); + REQUIRE(face->findGlyphInfo(glyph_a, EFontGlyphType::Grayscale) != nullptr); // Held by the head and by us: survives a sweep, glyph and all. gFontManagerp->collectGarbage(); - ensure("a held face keeps its glyphs across GC", - face->findGlyphInfo(glyph_a, EFontGlyphType::Grayscale) != nullptr); - ensure_equals("a held face is still the cached one", - gFontManagerp->getOrCreateFace(makeKey(path)).get(), face.get()); + UNSCOPED_INFO("a held face keeps its glyphs across GC"); + REQUIRE(face->findGlyphInfo(glyph_a, EFontGlyphType::Grayscale) != nullptr); + UNSCOPED_INFO("a held face is still the cached one"); + REQUIRE(gFontManagerp->getOrCreateFace(makeKey(path)).get() == face.get()); // Held by the cache alone: swept, so the reload is a fresh face. ft = nullptr; face = nullptr; gFontManagerp->collectGarbage(); LLPointer reloaded = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("face reloaded", reloaded.notNull() && reloaded->isValid()); - ensure("the reloaded face has no glyphs: the cached one was evicted", - reloaded->findGlyphInfo(glyph_a, EFontGlyphType::Grayscale) == nullptr); + { + INFO("face reloaded"); + REQUIRE(reloaded.notNull()); + CHECK(reloaded->isValid()); + } + UNSCOPED_INFO("the reloaded face has no glyphs: the cached one was evicted"); + REQUIRE(reloaded->findGlyphInfo(glyph_a, EFontGlyphType::Grayscale) == nullptr); } // After two getGlyphInfo calls in a row, both glyphs end up in @@ -159,26 +166,29 @@ namespace // font-atlas layer (the disable-on-setSubImage regression would // surface as a crash or as a generation skip on the second // upload). - template<> template<> - void llfontfreetype_render_object::test<4>() + TEST_CASE_METHOD(llfontfreetype_render_data, "LLFontFreetypeRender: two ASCII glyphs rasterized in a row share one sheet", "[LLFontFreetypeRender]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFtHead(path); - ensure("DejaVuSans loaded as head", ft.notNull()); + UNSCOPED_INFO("DejaVuSans loaded as head"); + REQUIRE(ft.notNull()); LLFontGlyphInfo* g1 = ft->getGlyphInfo(L'A', EFontGlyphType::Grayscale); LLFontGlyphInfo* g2 = ft->getGlyphInfo(L'B', EFontGlyphType::Grayscale); - ensure("first glyph allocated", g1 != nullptr); - ensure("second glyph allocated", g2 != nullptr); + UNSCOPED_INFO("first glyph allocated"); + REQUIRE(g1 != nullptr); + UNSCOPED_INFO("second glyph allocated"); + REQUIRE(g2 != nullptr); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - ensure("bitmap cache present", cache != nullptr); + UNSCOPED_INFO("bitmap cache present"); + REQUIRE(cache != nullptr); // Both glyphs should land in sheet 0 (a single 1024² sheet // can hold thousands of small ASCII glyphs). - ensure_equals("two ASCII glyphs share sheet 0", - cache->getNumBitmaps(EFontGlyphType::Grayscale), 1u); + UNSCOPED_INFO("two ASCII glyphs share sheet 0"); + CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) == 1u); } // Variable-axis weight metric effect: loading InterVariable at @@ -191,36 +201,37 @@ namespace // a >1px advance gap at 32pt 96dpi. Small point sizes can collapse // the diff under FT's grid-fitting; this guards explicitly against // that edge case. - template<> template<> - void llfontfreetype_render_object::test<5>() + TEST_CASE_METHOD(llfontfreetype_render_data, "LLFontFreetypeRender: InterVariable at weight 900 has a wider 'A' than at weight 300", "[LLFontFreetypeRender]") { const std::string path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(path)) - skip("InterVariable.woff2 not present"); + SKIP("InterVariable.woff2 not present"); ALFontVarAxes va_light; va_light.wght = 300.f; va_light.wght_set = true; LLPointer ft_light = new LLFontFreetype; - ensure("weight=300 load", - ft_light->loadFace(path, 32.f, 96.f, 96.f, + UNSCOPED_INFO("weight=300 load"); + CHECK(ft_light->loadFace(path, 32.f, 96.f, 96.f, /*is_fallback=*/false, 0, EFontHinting::DEFAULT, /*flags=*/0, va_light)); ALFontVarAxes va_black; va_black.wght = 900.f; va_black.wght_set = true; LLPointer ft_black = new LLFontFreetype; - ensure("weight=900 load", - ft_black->loadFace(path, 32.f, 96.f, 96.f, + UNSCOPED_INFO("weight=900 load"); + CHECK(ft_black->loadFace(path, 32.f, 96.f, 96.f, /*is_fallback=*/false, 0, EFontHinting::DEFAULT, /*flags=*/0, va_black)); const F32 adv_light = ft_light->getXAdvance(L'A'); const F32 adv_black = ft_black->getXAdvance(L'A'); - ensure("light advance positive", adv_light > 0.f); - ensure("black advance positive", adv_black > 0.f); + UNSCOPED_INFO("light advance positive"); + CHECK(adv_light > 0.f); + UNSCOPED_INFO("black advance positive"); + CHECK(adv_black > 0.f); // Heavier weight produces wider 'A'. The diff at 32pt 96dpi // between weight=300 and weight=900 is ~1.5-2 px for Inter. - ensure("weight=900 'A' advance > weight=300 'A' advance", - adv_black > adv_light + 0.5f); + UNSCOPED_INFO("weight=900 'A' advance > weight=300 'A' advance"); + CHECK(adv_black > adv_light + 0.5f); } // Sheet re-entry through the rasterizer: rasterize onto sheet 0, @@ -229,23 +240,25 @@ namespace // across eviction cycles) and getCacheGeneration advances so // captured vertex buffers rebuild. Pins slot recycling through the // rasterizer layer (the cache-layer tests pin it at nextOpenPos). - template<> template<> - void llfontfreetype_render_object::test<6>() + TEST_CASE_METHOD(llfontfreetype_render_data, "LLFontFreetypeRender: a glyph after sheet 0's release reuses the slot and bumps the generation", "[LLFontFreetypeRender]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFtHead(path); - ensure("DejaVuSans loaded as head", ft.notNull()); + UNSCOPED_INFO("DejaVuSans loaded as head"); + REQUIRE(ft.notNull()); LLFontGlyphInfo* g1 = ft->getGlyphInfo(L'A', EFontGlyphType::Grayscale); - ensure("first glyph allocated", g1 != nullptr); + UNSCOPED_INFO("first glyph allocated"); + REQUIRE(g1 != nullptr); // Use getBitmapCache() (non-const) for releaseSheet; the const // overload exposed via getFontBitmapCache() is for queries only. LLFontBitmapCache* cache = ft->getBitmapCache(); - ensure("bitmap cache present", cache != nullptr); - ensure_equals("started with one Grayscale sheet", - cache->getNumBitmaps(EFontGlyphType::Grayscale), 1u); + UNSCOPED_INFO("bitmap cache present"); + REQUIRE(cache != nullptr); + UNSCOPED_INFO("started with one Grayscale sheet"); + CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) == 1u); const S32 gen_before = cache->getCacheGeneration(); // Honor the purge-before-release contract the production sweep @@ -265,19 +278,20 @@ namespace return false; }); cache->releaseSheet(EFontGlyphType::Grayscale, 0); - ensure("sheet 0 reports released", - cache->isSheetReleased(EFontGlyphType::Grayscale, 0)); + UNSCOPED_INFO("sheet 0 reports released"); + CHECK(cache->isSheetReleased(EFontGlyphType::Grayscale, 0)); // New glyph rebuilds into the recycled slot. Picking a glyph not // already cached avoids hitting the cmap-cache branch. LLFontGlyphInfo* g2 = ft->getGlyphInfo(L'B', EFontGlyphType::Grayscale); - ensure("second glyph allocated post-release", g2 != nullptr); - ensure_equals("released slot recycled (no sheet-vector growth)", - cache->getNumBitmaps(EFontGlyphType::Grayscale), 1u); - ensure("recycled slot is live again", - !cache->isSheetReleased(EFontGlyphType::Grayscale, 0)); - ensure("cache generation advanced after release+alloc", - cache->getCacheGeneration() > gen_before); + UNSCOPED_INFO("second glyph allocated post-release"); + REQUIRE(g2 != nullptr); + UNSCOPED_INFO("released slot recycled (no sheet-vector growth)"); + CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) == 1u); + UNSCOPED_INFO("recycled slot is live again"); + CHECK_FALSE(cache->isSheetReleased(EFontGlyphType::Grayscale, 0)); + UNSCOPED_INFO("cache generation advanced after release+alloc"); + CHECK(cache->getCacheGeneration() > gen_before); } // Cross-instance global generation contention: rasterizing on @@ -286,25 +300,30 @@ namespace // ticks across faces (llfontbitmapcache.h:81-90) so that vertex // buffers using face B's atlas can detect changes that fired on // any face. - template<> template<> - void llfontfreetype_render_object::test<7>() + TEST_CASE_METHOD(llfontfreetype_render_data, "LLFontFreetypeRender: rasterizing on each of two faces bumps the global generation", "[LLFontFreetypeRender]") { const std::string a_path = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string b_path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(a_path) || !fileExists(b_path)) - skip("DejaVuSans + InterVariable required"); + SKIP("DejaVuSans + InterVariable required"); LLPointer head_a = loadFtHead(a_path); LLPointer head_b = loadFtHead(b_path); - ensure("both heads loaded", head_a.notNull() && head_b.notNull()); + { + INFO("both heads loaded"); + REQUIRE(head_a.notNull()); + REQUIRE(head_b.notNull()); + } const S32 gen0 = LLFontBitmapCache::getGlobalGeneration(); (void)head_a->getGlyphInfo(L'A', EFontGlyphType::Grayscale); const S32 gen1 = LLFontBitmapCache::getGlobalGeneration(); - ensure("gen advanced after head_a raster", gen1 > gen0); + UNSCOPED_INFO("gen advanced after head_a raster"); + CHECK(gen1 > gen0); (void)head_b->getGlyphInfo(L'B', EFontGlyphType::Grayscale); const S32 gen2 = LLFontBitmapCache::getGlobalGeneration(); - ensure("gen advanced after head_b raster", gen2 > gen1); + UNSCOPED_INFO("gen advanced after head_b raster"); + CHECK(gen2 > gen1); } // Cross-head sheet eviction: when one freetype's collectGarbage @@ -318,31 +337,40 @@ namespace // "glyphs unload after long idle and never reload"). The fix routes // every lookup through the face's owned cache so all freetypes that // share the face observe the eviction consistently. - template<> template<> - void llfontfreetype_render_object::test<8>() + TEST_CASE_METHOD(llfontfreetype_render_data, "LLFontFreetypeRender: a sibling head gets a live glyph after the other evicts its sheet", "[LLFontFreetypeRender]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer head_a = loadFtHead(path); LLPointer head_b = loadFtHead(path); - ensure("both heads loaded", head_a.notNull() && head_b.notNull()); + { + INFO("both heads loaded"); + REQUIRE(head_a.notNull()); + REQUIRE(head_b.notNull()); + } // Same ALFontFaceKey -> shared ALFontFace via getOrCreateFace. - ensure_equals("siblings share the underlying face wrapper", - head_a->getFontFace(), head_b->getFontFace()); + UNSCOPED_INFO("siblings share the underlying face wrapper"); + REQUIRE(head_a->getFontFace() == head_b->getFontFace()); // Cache a glyph through both heads. Face dedup means both // calls return the same entry pointer. LLFontGlyphInfo* a_gi = head_a->getGlyphInfo(L'A', EFontGlyphType::Grayscale); LLFontGlyphInfo* b_gi = head_b->getGlyphInfo(L'A', EFontGlyphType::Grayscale); - ensure("head_a got glyph", a_gi != nullptr); - ensure("head_b got glyph", b_gi != nullptr); - ensure_equals("shared face returns the same glyph entry", a_gi, b_gi); + UNSCOPED_INFO("head_a got glyph"); + REQUIRE(a_gi != nullptr); + UNSCOPED_INFO("head_b got glyph"); + REQUIRE(b_gi != nullptr); + UNSCOPED_INFO("shared face returns the same glyph entry"); + REQUIRE(a_gi == b_gi); const auto pre_entry = a_gi->mPhaseSlots[0].mBitmapEntry; - ensure("phase 0 bitmap entry references a real sheet", - pre_entry.first == EFontGlyphType::Grayscale && pre_entry.second >= 0); + { + INFO("phase 0 bitmap entry references a real sheet"); + CHECK(pre_entry.first == EFontGlyphType::Grayscale); + CHECK(pre_entry.second >= 0); + } // Simulate head_a's collectGarbage path on the shared face: // delete face-owned entries that reference this sheet, then @@ -351,7 +379,8 @@ namespace // pointer; post-fix there is no per-head cache to leak. const auto target = pre_entry; LLFontBitmapCache* cache = head_a->getBitmapCache(); - ensure("bitmap cache present", cache != nullptr); + UNSCOPED_INFO("bitmap cache present"); + REQUIRE(cache != nullptr); head_a->getFontFace()->erase_glyph_entries( [target](const LLFontGlyphInfo* gi) { @@ -364,19 +393,19 @@ namespace return false; }); cache->releaseSheet(target.first, static_cast(target.second)); - ensure("target sheet reports released", - cache->isSheetReleased(target.first, static_cast(target.second))); + UNSCOPED_INFO("target sheet reports released"); + CHECK(cache->isSheetReleased(target.first, static_cast(target.second))); // head_b looks up the same glyph again. The lookup must: // 1. not crash on a dangling pointer, // 2. produce a non-null entry, // 3. land on a live sheet (not the just-released one). LLFontGlyphInfo* b_gi2 = head_b->getGlyphInfo(L'A', EFontGlyphType::Grayscale); - ensure("head_b got fresh glyph after sibling-driven eviction", - b_gi2 != nullptr); + UNSCOPED_INFO("head_b got fresh glyph after sibling-driven eviction"); + REQUIRE(b_gi2 != nullptr); const auto post_entry = b_gi2->mPhaseSlots[0].mBitmapEntry; - ensure("post-eviction phase 0 references a live sheet", - !cache->isSheetReleased(post_entry.first, + UNSCOPED_INFO("post-eviction phase 0 references a live sheet"); + CHECK_FALSE(cache->isSheetReleased(post_entry.first, static_cast(post_entry.second))); } @@ -386,21 +415,25 @@ namespace // on a live sheet. (The pre-fix bug bit only the cross-head // scenario, but this test pins the single-head invariant so future // refactors of the eviction path don't silently regress.) - template<> template<> - void llfontfreetype_render_object::test<9>() + TEST_CASE_METHOD(llfontfreetype_render_data, "LLFontFreetypeRender: a glyph evicted with its sheet comes back on a live sheet", "[LLFontFreetypeRender]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFtHead(path); - ensure("ft loaded", ft.notNull()); + UNSCOPED_INFO("ft loaded"); + REQUIRE(ft.notNull()); LLFontGlyphInfo* g1 = ft->getGlyphInfo(L'Z', EFontGlyphType::Grayscale); - ensure("got glyph", g1 != nullptr); + UNSCOPED_INFO("got glyph"); + REQUIRE(g1 != nullptr); const auto target = g1->mPhaseSlots[0].mBitmapEntry; - ensure("bitmap entry valid", - target.first == EFontGlyphType::Grayscale && target.second >= 0); + { + INFO("bitmap entry valid"); + CHECK(target.first == EFontGlyphType::Grayscale); + CHECK(target.second >= 0); + } // Run the same delete + release dance the GC path uses. ft->getFontFace()->erase_glyph_entries( @@ -421,10 +454,11 @@ namespace // (now empty for this glyph), falls through to addShapedGlyphFromFont, // and rasterizes onto a live sheet. LLFontGlyphInfo* g2 = ft->getGlyphInfo(L'Z', EFontGlyphType::Grayscale); - ensure("re-requested glyph allocated post-eviction", g2 != nullptr); + UNSCOPED_INFO("re-requested glyph allocated post-eviction"); + REQUIRE(g2 != nullptr); const auto post = g2->mPhaseSlots[0].mBitmapEntry; - ensure("post-eviction bitmap entry references a live sheet", - !ft->getBitmapCache()->isSheetReleased( + UNSCOPED_INFO("post-eviction bitmap entry references a live sheet"); + CHECK_FALSE(ft->getBitmapCache()->isSheetReleased( post.first, static_cast(post.second))); } } diff --git a/indra/llrender/tests/llfontfreetype_test.cpp b/indra/llrender/tests/llfontfreetype_test.cpp index 6903243a8f..63d9ae8bbe 100644 --- a/indra/llrender/tests/llfontfreetype_test.cpp +++ b/indra/llrender/tests/llfontfreetype_test.cpp @@ -38,12 +38,10 @@ #include "llfonttest_helpers.h" -#include "../test/lltut.h" +#include #include -namespace tut -{ namespace { using namespace ll_test; @@ -57,402 +55,110 @@ namespace ~llfontfreetype_data() { LLFontManager::cleanupClass(); } }; - typedef test_group llfontfreetype_test; - typedef llfontfreetype_test::object llfontfreetype_object; - tut::llfontfreetype_test llfontfreetype_testcase("LLFontFreetype"); -} - - // initClass is idempotent — repeat calls don't replace gFontManagerp. - template<> template<> - void llfontfreetype_object::test<1>() - { - ensure("gFontManagerp non-null after init", gFontManagerp != nullptr); - LLFontManager* before = gFontManagerp; - LLFontManager::initClass(); // second call: no-op - ensure_equals("gFontManagerp unchanged on repeat initClass", - gFontManagerp, before); - } - - // getOrCreateFace returns the same face for equal keys; differing - // point_size produces a distinct face. Pins the cache identity - // contract that LLFontFreetype's mFace sharing relies on. - template<> template<> - void llfontfreetype_object::test<2>() - { - const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; - if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); - - LLPointer a = gFontManagerp->getOrCreateFace(makeKey(path, 14.f)); - LLPointer b = gFontManagerp->getOrCreateFace(makeKey(path, 14.f)); - LLPointer c = gFontManagerp->getOrCreateFace(makeKey(path, 18.f)); - ensure("face a loaded", a.notNull() && a->isValid()); - ensure("face b loaded", b.notNull() && b->isValid()); - ensure("face c loaded", c.notNull() && c->isValid()); - ensure_equals("equal keys -> same face pointer", - a.get(), b.get()); - ensure_not_equals("different point_size -> distinct face", - a.get(), c.get()); - } - - // loadFace on a real file returns true and populates getName. - template<> template<> - void llfontfreetype_object::test<3>() - { - const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; - if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); - LLPointer ft = loadFt(path); - ensure("loadFace succeeded", ft.notNull()); - ensure_equals("getName matches filename", ft->getName(), path); - } - - // loadFace on a missing file returns false; callers that own the - // LLFontFreetype must see a safe-to-destruct instance. - template<> template<> - void llfontfreetype_object::test<4>() - { - LLPointer ft = new LLFontFreetype; - const bool ok = ft->loadFace("does/not/exist.ttf", - 14.f, 96.f, 96.f, true, 0, - EFontHinting::DEFAULT, 0); - ensure("loadFace returns false on missing file", !ok); - // Don't deref ft beyond this — destructor must run cleanly. - ft = nullptr; - ensure("destruct after failed load did not crash", true); - } - - // getCharGlyphIndex returns non-zero for ASCII present in the cmap - // and zero for codepoints absent from the cmap. Second call hits - // the cache (no observable difference, but exercises the path). - template<> template<> - void llfontfreetype_object::test<5>() - { - const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; - if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); - LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); - const U32 a1 = ft->getCharGlyphIndex(L'A'); - const U32 a2 = ft->getCharGlyphIndex(L'A'); - ensure_not_equals("'A' has a non-zero glyph index", a1, 0u); - ensure_equals("repeated lookup returns same value (cache hit)", - a1, a2); - // U+E000 is in the BMP private-use area — DejaVuSans doesn't - // populate it; getCharGlyphIndex must return 0 (cached miss - // is meaningful — it lets the renderer fall through to - // fallbacks without re-walking the cmap). - ensure_equals("PUA codepoint has glyph index 0", - ft->getCharGlyphIndex(0xE000), 0u); - } - - // faceHasGlyph mirrors getCharGlyphIndex's notion of "covers this - // codepoint." Verify positive (ASCII present) and negative - // (Noto-COLRv1 lacks U+FE0F) cases — pins the VS-16 strip - // discriminator used by shape_sub_run. - template<> template<> - void llfontfreetype_object::test<6>() - { - const std::string dejavu = std::string(kFontDir) + "DejaVuSans.woff2"; - const std::string noto = std::string(kFontDir) + "Noto-COLRv1.ttf"; - if (!fileExists(dejavu) || !fileExists(noto)) - skip("DejaVuSans.woff2 + Noto-COLRv1.ttf both required"); - - LLPointer dv = loadFt(dejavu); - LLPointer nt = loadFt(noto); - ensure("both loaded", dv.notNull() && nt.notNull()); - ensure("DejaVuSans covers 'a'", dv->faceHasGlyph(L'a')); - ensure("Noto-COLRv1 lacks U+FE0F", - !nt->faceHasGlyph(0xFE0F)); - ensure("Noto-COLRv1 covers ZWJ (U+200D)", - nt->faceHasGlyph(0x200D)); - } - - // useSubpixelPen depends on hinting and color/svg flags: - // FORCE_AUTOHINT on a non-color font => true; DEFAULT hinting => false; - // a color font (regardless of hinting) => false. Pins the rule at - // alfontface.cpp:99. - template<> template<> - void llfontfreetype_object::test<7>() - { - const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; - if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); - - LLPointer a = loadFt(path, /*is_fallback=*/true, - 14.f, -1, EFontHinting::DEFAULT); - LLPointer b = loadFt(path, /*is_fallback=*/true, - 14.f, -1, EFontHinting::FORCE_AUTOHINT); - ensure("both loaded", a.notNull() && b.notNull()); - ensure("DEFAULT hinting -> useSubpixelPen=false", - !a->useSubpixelPen()); - ensure("FORCE_AUTOHINT on non-color font -> useSubpixelPen=true", - b->useSubpixelPen()); - - // A color font (Noto-COLRv1) with FORCE_AUTOHINT still reports - // false because the rule short-circuits on color/svg. - const std::string colr = std::string(kFontDir) + "Noto-COLRv1.ttf"; - if (fileExists(colr)) - { - LLPointer c = loadFt(colr, true, 14.f, -1, - EFontHinting::FORCE_AUTOHINT); - ensure("Noto-COLRv1 loaded", c.notNull()); - ensure("color font -> useSubpixelPen=false even with FORCE_AUTOHINT", - !c->useSubpixelPen()); - } - } - - // isFixedWidth: DejaVuSansMono yes, DejaVuSans no. - // getAllowMonospaceLigatures defaults false; setter toggles. - template<> template<> - void llfontfreetype_object::test<8>() - { - const std::string mono = std::string(kFontDir) + "DejaVuSansMono.woff2"; - const std::string prop = std::string(kFontDir) + "DejaVuSans.woff2"; - if (!fileExists(mono) || !fileExists(prop)) - skip("DejaVuSans + DejaVuSansMono required"); - - LLPointer m = loadFt(mono); - LLPointer p = loadFt(prop); - ensure("Mono is fixed-width", m->isFixedWidth()); - ensure("Prop is not fixed-width", !p->isFixedWidth()); - ensure("ligatures default off on Mono", - !m->getAllowMonospaceLigatures()); - m->setAllowMonospaceLigatures(true); - ensure("ligatures-on toggle takes effect", - m->getAllowMonospaceLigatures()); - m->setAllowMonospaceLigatures(false); - ensure("ligatures-off toggle takes effect", - !m->getAllowMonospaceLigatures()); - } - - // getStyle reflects the FT face's intrinsic flags. DejaVuSans is - // NORMAL; DejaVuSans-Bold has the BOLD bit set after load. - template<> template<> - void llfontfreetype_object::test<9>() - { - const std::string normal = std::string(kFontDir) + "DejaVuSans.woff2"; - const std::string bold = std::string(kFontDir) + "DejaVuSans-Bold.woff2"; - if (!fileExists(normal) || !fileExists(bold)) - skip("DejaVuSans + DejaVuSans-Bold required"); - - LLPointer n = loadFt(normal); - LLPointer b = loadFt(bold); - ensure("normal style has no BOLD bit", - (n->getStyle() & LLFontGL::BOLD) == 0); - ensure("bold file sets BOLD bit", - (b->getStyle() & LLFontGL::BOLD) != 0); - } - - // selectShapingFace walks the fallback chain by codepoint coverage: - // ASCII routes to the head, CJK routes to a CJK fallback, emoji - // routes to the emoji fallback. Pins multi-hop priority resolution - // at llfontfreetype.cpp:266-284. - template<> template<> - void llfontfreetype_object::test<10>() - { - const std::string dejavu = std::string(kFontDir) + "DejaVuSans.woff2"; - const std::string han = std::string(kFontDir) + "SourceHanSans-Regular.woff2"; - const std::string emoji = std::string(kFontDir) + "Noto-COLRv1.ttf"; - if (!fileExists(dejavu) || !fileExists(han) || !fileExists(emoji)) - skip("DejaVuSans + SourceHanSans + Noto-COLRv1 required"); - - LLPointer head = loadFt(dejavu); - LLPointer cjk = loadFt(han); - LLPointer emo = loadFt(emoji); - ensure("all three loaded", - head.notNull() && cjk.notNull() && emo.notNull()); - head->addFallbackFont(cjk); - head->addFallbackFont(emo); - - U32 idx = 0; - // ASCII 'a' lives in the head's cmap → head wins. - ensure_equals("'a' resolves to head", - head->selectShapingFace(L'a', idx), head.get()); - ensure_not_equals("head produced a non-zero glyph for 'a'", idx, 0u); - - // U+4F60 你 lives in SourceHanSans, not in DejaVuSans → CJK fallback. - idx = 0; - ensure_equals("CJK codepoint resolves to SourceHanSans fallback", - head->selectShapingFace(0x4F60, idx), cjk.get()); - - // U+1F525 (fire emoji) lives in Noto-COLRv1, not in either text - // face → emoji fallback wins. - idx = 0; - ensure_equals("emoji codepoint resolves to Noto-COLRv1 fallback", - head->selectShapingFace(0x1F525, idx), emo.get()); - } - - // mShapingFaceResolution caches selectShapingFace decisions; adding - // a new fallback must invalidate that cache so a previously-resolved - // codepoint can re-route through the newly-attached chain. Pins the - // cache-clear on addFallbackFont at llfontfreetype.cpp:434. - template<> template<> - void llfontfreetype_object::test<11>() - { - const std::string dejavu = std::string(kFontDir) + "DejaVuSans.woff2"; - const std::string emoji = std::string(kFontDir) + "Noto-COLRv1.ttf"; - if (!fileExists(dejavu) || !fileExists(emoji)) - skip("DejaVuSans + Noto-COLRv1 required"); - - LLPointer head = loadFt(dejavu); - LLPointer emo = loadFt(emoji); - ensure("both loaded", head.notNull() && emo.notNull()); - - // First lookup, no fallback registered. 'a' resolves to head; - // emoji resolves to head with idx=0 (no fallback covers it). - U32 idx_a1 = 0, idx_e1 = 0; - const LLFontFreetype* a1 = head->selectShapingFace(L'a', idx_a1); - const LLFontFreetype* e1 = head->selectShapingFace(0x1F525, idx_e1); - ensure_equals("'a' first resolution -> head", a1, head.get()); - ensure_equals("emoji first resolution -> head (no fallback)", - e1, head.get()); - ensure_equals("emoji on plain head produced glyph 0", - idx_e1, 0u); - - // Second lookup hits the cache and returns the same pointer. - U32 idx_a2 = 0; - ensure_equals("repeat 'a' lookup hits cache", - head->selectShapingFace(L'a', idx_a2), head.get()); - - // Add the emoji fallback. The cache must invalidate so the next - // emoji lookup re-runs the walker through the new chain. - head->addFallbackFont(emo); - - U32 idx_e2 = 0; - ensure_equals("after addFallbackFont, emoji re-resolves to fallback", - head->selectShapingFace(0x1F525, idx_e2), emo.get()); - } - - // Functor gating: a fallback whose char_functor returns false for - // codepoint X is excluded from X's resolution, even if its cmap - // covers X. Pins the must-accept gate at llfontfreetype.cpp:268-273. - template<> template<> - void llfontfreetype_object::test<12>() - { - const std::string dejavu = std::string(kFontDir) + "DejaVuSans.woff2"; - const std::string emoji = std::string(kFontDir) + "TwemojiSVG.woff2"; - if (!fileExists(dejavu) || !fileExists(emoji)) - skip("DejaVuSans + TwemojiSVG required"); - - LLPointer head = loadFt(dejavu); - LLPointer emo = loadFt(emoji); - ensure("both loaded", head.notNull() && emo.notNull()); - - // Functor accepts only emoji-range codepoints; rejects ASCII. - // If TwemojiSVG happens to carry ASCII outlines, the functor - // forces the head to win 'a' regardless. - auto pictograph_only = [](llwchar c) -> bool { return c >= 0x1F000; }; - head->addFallbackFont(emo, pictograph_only); - - U32 idx = 0; - ensure_equals("'a' resolves to head despite Twemoji ASCII coverage", - head->selectShapingFace(L'a', idx), head.get()); - // Functor allows emoji range, so emoji codepoints route to Twemoji. - idx = 0; - ensure_equals("emoji codepoint passes functor and resolves to Twemoji", - head->selectShapingFace(0x1F525, idx), emo.get()); - } // ------------------------------------------------------------- // ALFontFace group: identity key, COLR/SVG/color/wght probes, // cmap cache, refcount sharing semantics. // ------------------------------------------------------------- -namespace -{ struct alfontface_data { alfontface_data() { LLFontManager::initClass(); } ~alfontface_data() { LLFontManager::cleanupClass(); } }; - typedef test_group alfontface_test; - typedef alfontface_test::object alfontface_object; - tut::alfontface_test alfontface_testcase("ALFontFace"); -} // ALFontFaceKey equality + hash: equal keys equal+hash-equal; // any single-field difference produces unequal keys. - template<> template<> - void alfontface_object::test<1>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: ALFontFaceKey copies are equal and hash alike; one changed field is not", "[ALFontFace]") { ALFontFaceKey a{ "x.ttf", 0, 14.f, 96.f, 96.f, EFontHinting::DEFAULT, 0 }; ALFontFaceKey b = a; - ensure("identical keys equal", - a == b); - ensure_equals("identical keys hash equal", - hash_value(a), hash_value(b)); + UNSCOPED_INFO("identical keys equal"); + CHECK(a == b); + UNSCOPED_INFO("identical keys hash equal"); + CHECK(hash_value(a) == hash_value(b)); b.point_size = 18.f; - ensure("differing point_size keys not equal", !(a == b)); + UNSCOPED_INFO("differing point_size keys not equal"); + CHECK_FALSE(a == b); b = a; b.face_index = 1; - ensure("differing face_index keys not equal", !(a == b)); + UNSCOPED_INFO("differing face_index keys not equal"); + CHECK_FALSE(a == b); b = a; b.var_axes.wght = 600.f; b.var_axes.wght_set = true; - ensure("differing wght keys not equal", !(a == b)); + UNSCOPED_INFO("differing wght keys not equal"); + CHECK_FALSE(a == b); b = a; b.hinting = EFontHinting::FORCE_AUTOHINT; - ensure("differing hinting keys not equal", !(a == b)); + UNSCOPED_INFO("differing hinting keys not equal"); + CHECK_FALSE(a == b); b = a; b.flags = LLFontGL::BOLD; - ensure("differing flags keys not equal", !(a == b)); + UNSCOPED_INFO("differing flags keys not equal"); + CHECK_FALSE(a == b); } // hasColrV1: Noto-COLRv1 yes, DejaVuSans no. - template<> template<> - void alfontface_object::test<2>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: hasColrV1 is true for Noto-COLRv1 and false for DejaVuSans", "[ALFontFace]") { const std::string noto = std::string(kFontDir) + "Noto-COLRv1.ttf"; const std::string dv = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(noto) || !fileExists(dv)) - skip("Noto-COLRv1 + DejaVuSans required"); + SKIP("Noto-COLRv1 + DejaVuSans required"); LLPointer nf = gFontManagerp->getOrCreateFace(makeKey(noto)); LLPointer df = gFontManagerp->getOrCreateFace(makeKey(dv)); - ensure("both faces valid", nf->isValid() && df->isValid()); - ensure("Noto-COLRv1 hasColrV1", nf->hasColrV1()); - ensure("DejaVuSans !hasColrV1", !df->hasColrV1()); + { + INFO("both faces valid"); + CHECK(nf->isValid()); + CHECK(df->isValid()); + } + UNSCOPED_INFO("Noto-COLRv1 hasColrV1"); + CHECK(nf->hasColrV1()); + UNSCOPED_INFO("DejaVuSans !hasColrV1"); + CHECK_FALSE(df->hasColrV1()); } // hasColor: Noto-COLRv1 yes (FT_HAS_COLOR is set for any color // table); DejaVuSans no. - template<> template<> - void alfontface_object::test<3>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: hasColor is true for Noto-COLRv1 and false for DejaVuSans", "[ALFontFace]") { const std::string noto = std::string(kFontDir) + "Noto-COLRv1.ttf"; const std::string dv = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(noto) || !fileExists(dv)) - skip("Noto-COLRv1 + DejaVuSans required"); + SKIP("Noto-COLRv1 + DejaVuSans required"); LLPointer nf = gFontManagerp->getOrCreateFace(makeKey(noto)); LLPointer df = gFontManagerp->getOrCreateFace(makeKey(dv)); - ensure("Noto-COLRv1 hasColor", nf->hasColor()); - ensure("DejaVuSans !hasColor", !df->hasColor()); + UNSCOPED_INFO("Noto-COLRv1 hasColor"); + CHECK(nf->hasColor()); + UNSCOPED_INFO("DejaVuSans !hasColor"); + CHECK_FALSE(df->hasColor()); } // hasSvg: Noto-COLRv1 doesn't ship OT-SVG; DejaVuSans doesn't // either. Both should report false. (TwemojiSVG would be the // positive case but isn't checked into the fixture set.) - template<> template<> - void alfontface_object::test<4>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: hasSvg is false for both Noto-COLRv1 and DejaVuSans", "[ALFontFace]") { const std::string noto = std::string(kFontDir) + "Noto-COLRv1.ttf"; const std::string dv = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(noto) || !fileExists(dv)) - skip("Noto-COLRv1 + DejaVuSans required"); + SKIP("Noto-COLRv1 + DejaVuSans required"); LLPointer nf = gFontManagerp->getOrCreateFace(makeKey(noto)); LLPointer df = gFontManagerp->getOrCreateFace(makeKey(dv)); - ensure("Noto-COLRv1 !hasSvg", !nf->hasSvg()); - ensure("DejaVuSans !hasSvg", !df->hasSvg()); + UNSCOPED_INFO("Noto-COLRv1 !hasSvg"); + CHECK_FALSE(nf->hasSvg()); + UNSCOPED_INFO("DejaVuSans !hasSvg"); + CHECK_FALSE(df->hasSvg()); } // wghtAxisSet: InterVariable is variable; loading at weight=600 // sets the wght axis. Loading at weight=-1 leaves wght axis off. - template<> template<> - void alfontface_object::test<5>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: wghtAxisSet is set by a weight on InterVariable and not without one", "[ALFontFace]") { const std::string path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(path)) - skip("InterVariable.woff2 not present"); + SKIP("InterVariable.woff2 not present"); ALFontFaceKey k_default = makeKey(path); // wght not set -> face takes the file's default @@ -462,98 +168,104 @@ namespace LLPointer def = gFontManagerp->getOrCreateFace(k_default); LLPointer bld = gFontManagerp->getOrCreateFace(k_bold); - ensure("both loaded", def->isValid() && bld->isValid()); - ensure("wght unset -> wghtAxisSet=false", !def->wghtAxisSet()); - ensure("wght=600 on variable face -> wghtAxisSet=true", - bld->wghtAxisSet()); + { + INFO("both loaded"); + CHECK(def->isValid()); + CHECK(bld->isValid()); + } + UNSCOPED_INFO("wght unset -> wghtAxisSet=false"); + CHECK_FALSE(def->wghtAxisSet()); + UNSCOPED_INFO("wght=600 on variable face -> wghtAxisSet=true"); + CHECK(bld->wghtAxisSet()); } // isFixedWidth (face-level): DejaVuSansMono yes, DejaVuSans no. // Mirrors the LLFontFreetype-level test but exercises the // wrapper's mIsFixedWidth field directly. - template<> template<> - void alfontface_object::test<6>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: isFixedWidth is true for DejaVuSansMono and false for DejaVuSans", "[ALFontFace]") { const std::string mono = std::string(kFontDir) + "DejaVuSansMono.woff2"; const std::string prop = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(mono) || !fileExists(prop)) - skip("DejaVuSans + DejaVuSansMono required"); + SKIP("DejaVuSans + DejaVuSansMono required"); LLPointer m = gFontManagerp->getOrCreateFace(makeKey(mono)); LLPointer p = gFontManagerp->getOrCreateFace(makeKey(prop)); - ensure("Mono isFixedWidth", m->isFixedWidth()); - ensure("Prop !isFixedWidth", !p->isFixedWidth()); + UNSCOPED_INFO("Mono isFixedWidth"); + CHECK(m->isFixedWidth()); + UNSCOPED_INFO("Prop !isFixedWidth"); + CHECK_FALSE(p->isFixedWidth()); } // getCharGlyphIndex matches FT_Get_Char_Index for present glyphs; // returns 0 for absent ones. Also the caching path: a 0 result // is just as cacheable as a non-zero result. - template<> template<> - void alfontface_object::test<7>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: getCharGlyphIndex is stable for a present glyph and 0 for an absent one", "[ALFontFace]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer f = gFontManagerp->getOrCreateFace(makeKey(path)); const U32 g_a = f->getCharGlyphIndex(L'A'); const U32 g_a_again = f->getCharGlyphIndex(L'A'); - ensure_not_equals("'A' has a glyph", g_a, 0u); - ensure_equals("repeat 'A' lookup hits cache (same value)", - g_a, g_a_again); - ensure_equals("absent codepoint -> 0", - f->getCharGlyphIndex(0xE000), 0u); + UNSCOPED_INFO("'A' has a glyph"); + CHECK(g_a != 0u); + UNSCOPED_INFO("repeat 'A' lookup hits cache (same value)"); + CHECK(g_a == g_a_again); + UNSCOPED_INFO("absent codepoint -> 0"); + CHECK(f->getCharGlyphIndex(0xE000) == 0u); // Cache the miss too — second call should also be 0. - ensure_equals("absent codepoint cached miss is also 0", - f->getCharGlyphIndex(0xE000), 0u); + UNSCOPED_INFO("absent codepoint cached miss is also 0"); + CHECK(f->getCharGlyphIndex(0xE000) == 0u); } // Refcount sharing: two LLFontFreetype instances that point at // the same ALFontFace via getOrCreateFace must keep the face // alive while either lives. The face cache also holds a ref, // so getNumRefs >= 3 (cache + a + b) when both shares exist. - template<> template<> - void alfontface_object::test<8>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: two lookups share a face and the cache keeps it after both let go", "[ALFontFace]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer a = gFontManagerp->getOrCreateFace(makeKey(path)); LLPointer b = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure_equals("a == b (shared face)", a.get(), b.get()); - ensure("refcount >= 3 (cache + a + b)", - a->getNumRefs() >= 3); + UNSCOPED_INFO("a == b (shared face)"); + REQUIRE(a.get() == b.get()); + UNSCOPED_INFO("refcount >= 3 (cache + a + b)"); + CHECK(a->getNumRefs() >= 3); ALFontFace* raw = a.get(); b = nullptr; // drop b's ref - ensure("face still alive after b dropped", - raw->getNumRefs() >= 2); + UNSCOPED_INFO("face still alive after b dropped"); + CHECK(raw->getNumRefs() >= 2); a = nullptr; // drop a's ref // Cache still holds a ref, so the face is alive but has // refcount 1. Verify by re-fetching: should return the same // pointer (cache hit), not a freshly-loaded face. LLPointer c = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure_equals("cache kept face alive after both refs dropped", - c.get(), raw); + UNSCOPED_INFO("cache kept face alive after both refs dropped"); + REQUIRE(c.get() == raw); } // resetBitmapCache clears atlas state without destroying the face. // A subsequent getCharGlyphIndex still works (cmap survives). - template<> template<> - void alfontface_object::test<9>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: resetBitmapCache keeps the face valid with its cmap and its atlas", "[ALFontFace]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer f = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("face valid before reset", f->isValid()); + UNSCOPED_INFO("face valid before reset"); + CHECK(f->isValid()); f->resetBitmapCache(); - ensure("face still valid after resetBitmapCache", - f->isValid()); - ensure("cmap survives reset", - f->getCharGlyphIndex(L'A') != 0u); - ensure("bitmap cache pointer still present", - f->getBitmapCache() != nullptr); + UNSCOPED_INFO("face still valid after resetBitmapCache"); + CHECK(f->isValid()); + UNSCOPED_INFO("cmap survives reset"); + CHECK(f->getCharGlyphIndex(L'A') != 0u); + UNSCOPED_INFO("bitmap cache pointer still present"); + REQUIRE(f->getBitmapCache() != nullptr); } // paletteIndex() is computed at load time from LLFontGL::sUseDarkEmojiPalette @@ -562,12 +274,11 @@ namespace // FT_PALETTE_FOR_DARK_BACKGROUND palette the second load still returns 0 // and we skip the equality check rather than fail. Pins b6ba746728's // load-time read at alfontface.cpp:122-137. - template<> template<> - void alfontface_object::test<10>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: paletteIndex follows sUseDarkEmojiPalette as it was at load", "[ALFontFace]") { const std::string noto = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(noto)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); const bool saved_flag = LLFontGL::sUseDarkEmojiPalette; @@ -578,10 +289,14 @@ namespace U32 pi_default; { LLPointer f = gFontManagerp->getOrCreateFace(k); - ensure("face valid (dark=false)", f.notNull() && f->isValid()); + { + INFO("face valid (dark=false)"); + REQUIRE(f.notNull()); + CHECK(f->isValid()); + } pi_default = f->paletteIndex(); - ensure_equals("paletteIndex defaults to 0 with dark=false", - pi_default, 0u); + UNSCOPED_INFO("paletteIndex defaults to 0 with dark=false"); + CHECK(pi_default == 0u); } // Drop our ref; collectGarbage evicts since cache holds the only ref. gFontManagerp->collectGarbage(); @@ -590,7 +305,11 @@ namespace U32 pi_dark; { LLPointer f = gFontManagerp->getOrCreateFace(k); - ensure("face valid (dark=true)", f.notNull() && f->isValid()); + { + INFO("face valid (dark=true)"); + REQUIRE(f.notNull()); + CHECK(f->isValid()); + } pi_dark = f->paletteIndex(); } @@ -602,42 +321,44 @@ namespace // does, pi_dark is non-zero (the index of that palette); if it // doesn't, pi_dark stays 0 and we skip the assertion. if (pi_dark == 0) - skip("font has no FT_PALETTE_FOR_DARK_BACKGROUND palette"); + SKIP("font has no FT_PALETTE_FOR_DARK_BACKGROUND palette"); - ensure_not_equals("dark=true selected a non-zero palette index", - pi_dark, 0u); + UNSCOPED_INFO("dark=true selected a non-zero palette index"); + CHECK(pi_dark != 0u); } // Variable axis on a non-variable font: requesting weight=600 on // DejaVuSans (which lacks a wght axis) leaves wghtAxisSet=false — // setVariationAxis returned false and the load fell through. Pins // the negative branch at alfontface.cpp:139-146. - template<> template<> - void alfontface_object::test<11>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: a weight on a face with no wght axis leaves wghtAxisSet false", "[ALFontFace]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); ALFontFaceKey k = makeKey(path); k.var_axes.wght = 600.f; k.var_axes.wght_set = true; LLPointer f = gFontManagerp->getOrCreateFace(k); - ensure("face loaded", f.notNull() && f->isValid()); - ensure("non-variable face leaves wghtAxisSet=false", - !f->wghtAxisSet()); + { + INFO("face loaded"); + REQUIRE(f.notNull()); + CHECK(f->isValid()); + } + UNSCOPED_INFO("non-variable face leaves wghtAxisSet=false"); + CHECK_FALSE(f->wghtAxisSet()); } // FaceKey weight is part of the identity key — distinct weight // values map to distinct face cache entries even on the same file. // Existing test 5 confirms the wghtAxisSet flag side; this pins // the cache-identity side at alfontface.h:64-76. - template<> template<> - void alfontface_object::test<12>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: distinct weights make distinct keys and distinct cached faces", "[ALFontFace]") { const std::string path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(path)) - skip("InterVariable.woff2 not present"); + SKIP("InterVariable.woff2 not present"); ALFontFaceKey k_400 = makeKey(path); k_400.var_axes.wght = 400.f; @@ -646,15 +367,20 @@ namespace k_600.var_axes.wght = 600.f; k_600.var_axes.wght_set = true; - ensure("k_400 != k_600", !(k_400 == k_600)); - ensure_not_equals("hash differs by weight", - hash_value(k_400), hash_value(k_600)); + UNSCOPED_INFO("k_400 != k_600"); + CHECK_FALSE(k_400 == k_600); + UNSCOPED_INFO("hash differs by weight"); + CHECK(hash_value(k_400) != hash_value(k_600)); LLPointer f400 = gFontManagerp->getOrCreateFace(k_400); LLPointer f600 = gFontManagerp->getOrCreateFace(k_600); - ensure("both faces valid", f400->isValid() && f600->isValid()); - ensure_not_equals("distinct weights produce distinct face entries", - f400.get(), f600.get()); + { + INFO("both faces valid"); + CHECK(f400->isValid()); + CHECK(f600->isValid()); + } + UNSCOPED_INFO("distinct weights produce distinct face entries"); + REQUIRE(f400.get() != f600.get()); } // ital/wdth/slnt axis plumbing: distinct ALFontVarAxes values must @@ -663,12 +389,11 @@ namespace // to the key. The bundled fonts don't actually expose ital/wdth/ // slnt axes (so setVariationAxis silently no-ops), but the key // identity check fires regardless of whether the axis takes effect. - template<> template<> - void alfontface_object::test<13>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: ital and wdth and slnt values are part of the key and its hash", "[ALFontFace]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); ALFontFaceKey k_default = makeKey(path); ALFontFaceKey k_ital = makeKey(path); @@ -681,32 +406,42 @@ namespace k_slnt.var_axes.slnt = -12.f; k_slnt.var_axes.slnt_set = true; - ensure("default != ital", !(k_default == k_ital)); - ensure("default != wdth", !(k_default == k_wdth)); - ensure("default != slnt", !(k_default == k_slnt)); - ensure("ital != wdth", !(k_ital == k_wdth)); - ensure("wdth != slnt", !(k_wdth == k_slnt)); - ensure_not_equals("default hash differs from ital", - hash_value(k_default), hash_value(k_ital)); - ensure_not_equals("default hash differs from wdth", - hash_value(k_default), hash_value(k_wdth)); - ensure_not_equals("default hash differs from slnt", - hash_value(k_default), hash_value(k_slnt)); + UNSCOPED_INFO("default != ital"); + CHECK_FALSE(k_default == k_ital); + UNSCOPED_INFO("default != wdth"); + CHECK_FALSE(k_default == k_wdth); + UNSCOPED_INFO("default != slnt"); + CHECK_FALSE(k_default == k_slnt); + UNSCOPED_INFO("ital != wdth"); + CHECK_FALSE(k_ital == k_wdth); + UNSCOPED_INFO("wdth != slnt"); + CHECK_FALSE(k_wdth == k_slnt); + UNSCOPED_INFO("default hash differs from ital"); + CHECK(hash_value(k_default) != hash_value(k_ital)); + UNSCOPED_INFO("default hash differs from wdth"); + CHECK(hash_value(k_default) != hash_value(k_wdth)); + UNSCOPED_INFO("default hash differs from slnt"); + CHECK(hash_value(k_default) != hash_value(k_slnt)); // Identical axis values produce equal keys + equal hashes. ALFontFaceKey k_ital_dup = makeKey(path); k_ital_dup.var_axes.ital = 1.f; k_ital_dup.var_axes.ital_set = true; - ensure("ital == ital_dup", k_ital == k_ital_dup); - ensure_equals("ital hash matches dup", - hash_value(k_ital), hash_value(k_ital_dup)); + UNSCOPED_INFO("ital == ital_dup"); + CHECK(k_ital == k_ital_dup); + UNSCOPED_INFO("ital hash matches dup"); + CHECK(hash_value(k_ital) == hash_value(k_ital_dup)); // Cache identity round-trips through the face manager. LLPointer fd = gFontManagerp->getOrCreateFace(k_default); LLPointer fi = gFontManagerp->getOrCreateFace(k_ital); - ensure("both faces valid", fd->isValid() && fi->isValid()); - ensure_not_equals("distinct ital settings produce distinct face entries", - fd.get(), fi.get()); + { + INFO("both faces valid"); + CHECK(fd->isValid()); + CHECK(fi->isValid()); + } + UNSCOPED_INFO("distinct ital settings produce distinct face entries"); + REQUIRE(fd.get() != fi.get()); } // Faces that don't expose the requested axis leave the matching @@ -714,12 +449,11 @@ namespace // don't carry ital/wdth/slnt; verify the silent-no-op contract // and that requesting an unsupported axis doesn't poison the // existing wghtAxisSet path. - template<> template<> - void alfontface_object::test<14>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: axes a face lacks leave its ital and wdth and slnt flags unset", "[ALFontFace]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); ALFontFaceKey k = makeKey(path); k.var_axes.ital = 1.f; @@ -730,38 +464,369 @@ namespace k.var_axes.slnt_set = true; LLPointer f = gFontManagerp->getOrCreateFace(k); - ensure("face loaded", f.notNull() && f->isValid()); - ensure("DejaVu lacks ital axis -> italAxisSet false", - !f->italAxisSet()); - ensure("DejaVu lacks wdth axis -> wdthAxisSet false", - !f->wdthAxisSet()); - ensure("DejaVu lacks slnt axis -> slntAxisSet false", - !f->slntAxisSet()); + { + INFO("face loaded"); + REQUIRE(f.notNull()); + CHECK(f->isValid()); + } + UNSCOPED_INFO("DejaVu lacks ital axis -> italAxisSet false"); + CHECK_FALSE(f->italAxisSet()); + UNSCOPED_INFO("DejaVu lacks wdth axis -> wdthAxisSet false"); + CHECK_FALSE(f->wdthAxisSet()); + UNSCOPED_INFO("DejaVu lacks slnt axis -> slntAxisSet false"); + CHECK_FALSE(f->slntAxisSet()); } // ALFontVarAxes equality: the comparison ignores value fields when // *_set is false (an unset axis MUST equal another unset axis with // a different stale value). Required so cache lookups for "no axes // set" don't depend on uninitialised float bits. - template<> template<> - void alfontface_object::test<15>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: ALFontVarAxes equality ignores the values of axes that are not set", "[ALFontFace]") { ALFontVarAxes a; ALFontVarAxes b; - ensure("default ctor equal", a == b); + UNSCOPED_INFO("default ctor equal"); + CHECK(a == b); // Differing values with *_set=false still equal. b.ital = 0.5f; b.wdth = 99.f; b.slnt = -3.f; - ensure("unset values are ignored in equality", a == b); + UNSCOPED_INFO("unset values are ignored in equality"); + CHECK(a == b); // Once a set flag flips, value differences matter. a.ital_set = true; a.ital = 1.f; b.ital_set = true; b.ital = 0.5f; - ensure("set ital with different values is not equal", - !(a == b)); + UNSCOPED_INFO("set ital with different values is not equal"); + CHECK_FALSE(a == b); b.ital = 1.f; - ensure("set ital with matching values is equal", a == b); + UNSCOPED_INFO("set ital with matching values is equal"); + CHECK(a == b); + } + + // initClass is idempotent — repeat calls don't replace gFontManagerp. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: initClass is idempotent and keeps the same gFontManagerp", "[LLFontFreetype]") + { + UNSCOPED_INFO("gFontManagerp non-null after init"); + REQUIRE(gFontManagerp != nullptr); + LLFontManager* before = gFontManagerp; + LLFontManager::initClass(); // second call: no-op + UNSCOPED_INFO("gFontManagerp unchanged on repeat initClass"); + REQUIRE(gFontManagerp == before); + } + + // getOrCreateFace returns the same face for equal keys; differing + // point_size produces a distinct face. Pins the cache identity + // contract that LLFontFreetype's mFace sharing relies on. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: getOrCreateFace shares a face for equal keys but not across point sizes", "[LLFontFreetype]") + { + const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; + if (!fileExists(path)) + SKIP("DejaVuSans.woff2 not present"); + + LLPointer a = gFontManagerp->getOrCreateFace(makeKey(path, 14.f)); + LLPointer b = gFontManagerp->getOrCreateFace(makeKey(path, 14.f)); + LLPointer c = gFontManagerp->getOrCreateFace(makeKey(path, 18.f)); + { + INFO("face a loaded"); + REQUIRE(a.notNull()); + CHECK(a->isValid()); + } + { + INFO("face b loaded"); + REQUIRE(b.notNull()); + CHECK(b->isValid()); + } + { + INFO("face c loaded"); + REQUIRE(c.notNull()); + CHECK(c->isValid()); + } + UNSCOPED_INFO("equal keys -> same face pointer"); + REQUIRE(a.get() == b.get()); + UNSCOPED_INFO("different point_size -> distinct face"); + REQUIRE(a.get() != c.get()); + } + + // loadFace on a real file returns true and populates getName. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: loadFace on a real file succeeds and getName is its path", "[LLFontFreetype]") + { + const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; + if (!fileExists(path)) + SKIP("DejaVuSans.woff2 not present"); + LLPointer ft = loadFt(path); + UNSCOPED_INFO("loadFace succeeded"); + REQUIRE(ft.notNull()); + UNSCOPED_INFO("getName matches filename"); + CHECK(ft->getName() == path); + } + + // loadFace on a missing file returns false; callers that own the + // LLFontFreetype must see a safe-to-destruct instance. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: loadFace on a missing file fails and the instance destructs cleanly", "[LLFontFreetype]") + { + LLPointer ft = new LLFontFreetype; + const bool ok = ft->loadFace("does/not/exist.ttf", + 14.f, 96.f, 96.f, true, 0, + EFontHinting::DEFAULT, 0); + UNSCOPED_INFO("loadFace returns false on missing file"); + CHECK_FALSE(ok); + // Don't deref ft beyond this — destructor must run cleanly. + ft = nullptr; + UNSCOPED_INFO("destruct after failed load did not crash"); + CHECK(true); + } + + // getCharGlyphIndex returns non-zero for ASCII present in the cmap + // and zero for codepoints absent from the cmap. Second call hits + // the cache (no observable difference, but exercises the path). + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: getCharGlyphIndex finds ASCII and is 0 for a private-use codepoint", "[LLFontFreetype]") + { + const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; + if (!fileExists(path)) + SKIP("DejaVuSans.woff2 not present"); + LLPointer ft = loadFt(path); + UNSCOPED_INFO("DejaVuSans loaded"); + REQUIRE(ft.notNull()); + const U32 a1 = ft->getCharGlyphIndex(L'A'); + const U32 a2 = ft->getCharGlyphIndex(L'A'); + UNSCOPED_INFO("'A' has a non-zero glyph index"); + CHECK(a1 != 0u); + UNSCOPED_INFO("repeated lookup returns same value (cache hit)"); + CHECK(a1 == a2); + // U+E000 is in the BMP private-use area — DejaVuSans doesn't + // populate it; getCharGlyphIndex must return 0 (cached miss + // is meaningful — it lets the renderer fall through to + // fallbacks without re-walking the cmap). + UNSCOPED_INFO("PUA codepoint has glyph index 0"); + CHECK(ft->getCharGlyphIndex(0xE000) == 0u); + } + + // faceHasGlyph mirrors getCharGlyphIndex's notion of "covers this + // codepoint." Verify positive (ASCII present) and negative + // (Noto-COLRv1 lacks U+FE0F) cases — pins the VS-16 strip + // discriminator used by shape_sub_run. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: faceHasGlyph: DejaVuSans has 'a'; Noto-COLRv1 has ZWJ but not U+FE0F", "[LLFontFreetype]") + { + const std::string dejavu = std::string(kFontDir) + "DejaVuSans.woff2"; + const std::string noto = std::string(kFontDir) + "Noto-COLRv1.ttf"; + if (!fileExists(dejavu) || !fileExists(noto)) + SKIP("DejaVuSans.woff2 + Noto-COLRv1.ttf both required"); + + LLPointer dv = loadFt(dejavu); + LLPointer nt = loadFt(noto); + { + INFO("both loaded"); + REQUIRE(dv.notNull()); + REQUIRE(nt.notNull()); + } + UNSCOPED_INFO("DejaVuSans covers 'a'"); + CHECK(dv->faceHasGlyph(L'a')); + UNSCOPED_INFO("Noto-COLRv1 lacks U+FE0F"); + CHECK_FALSE(nt->faceHasGlyph(0xFE0F)); + UNSCOPED_INFO("Noto-COLRv1 covers ZWJ (U+200D)"); + CHECK(nt->faceHasGlyph(0x200D)); + } + + // useSubpixelPen depends on hinting and color/svg flags: + // FORCE_AUTOHINT on a non-color font => true; DEFAULT hinting => false; + // a color font (regardless of hinting) => false. Pins the rule at + // alfontface.cpp:99. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: useSubpixelPen only for an autohinted face without color", "[LLFontFreetype]") + { + const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; + if (!fileExists(path)) + SKIP("DejaVuSans.woff2 not present"); + + LLPointer a = loadFt(path, /*is_fallback=*/true, + 14.f, -1, EFontHinting::DEFAULT); + LLPointer b = loadFt(path, /*is_fallback=*/true, + 14.f, -1, EFontHinting::FORCE_AUTOHINT); + { + INFO("both loaded"); + REQUIRE(a.notNull()); + REQUIRE(b.notNull()); + } + UNSCOPED_INFO("DEFAULT hinting -> useSubpixelPen=false"); + CHECK_FALSE(a->useSubpixelPen()); + UNSCOPED_INFO("FORCE_AUTOHINT on non-color font -> useSubpixelPen=true"); + CHECK(b->useSubpixelPen()); + + // A color font (Noto-COLRv1) with FORCE_AUTOHINT still reports + // false because the rule short-circuits on color/svg. + const std::string colr = std::string(kFontDir) + "Noto-COLRv1.ttf"; + if (fileExists(colr)) + { + LLPointer c = loadFt(colr, true, 14.f, -1, + EFontHinting::FORCE_AUTOHINT); + UNSCOPED_INFO("Noto-COLRv1 loaded"); + REQUIRE(c.notNull()); + UNSCOPED_INFO("color font -> useSubpixelPen=false even with FORCE_AUTOHINT"); + CHECK_FALSE(c->useSubpixelPen()); + } + } + + // isFixedWidth: DejaVuSansMono yes, DejaVuSans no. + // getAllowMonospaceLigatures defaults false; setter toggles. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: isFixedWidth only for DejaVuSansMono; monospace ligatures off until set", "[LLFontFreetype]") + { + const std::string mono = std::string(kFontDir) + "DejaVuSansMono.woff2"; + const std::string prop = std::string(kFontDir) + "DejaVuSans.woff2"; + if (!fileExists(mono) || !fileExists(prop)) + SKIP("DejaVuSans + DejaVuSansMono required"); + + LLPointer m = loadFt(mono); + LLPointer p = loadFt(prop); + UNSCOPED_INFO("Mono is fixed-width"); + CHECK(m->isFixedWidth()); + UNSCOPED_INFO("Prop is not fixed-width"); + CHECK_FALSE(p->isFixedWidth()); + UNSCOPED_INFO("ligatures default off on Mono"); + CHECK_FALSE(m->getAllowMonospaceLigatures()); + m->setAllowMonospaceLigatures(true); + UNSCOPED_INFO("ligatures-on toggle takes effect"); + CHECK(m->getAllowMonospaceLigatures()); + m->setAllowMonospaceLigatures(false); + UNSCOPED_INFO("ligatures-off toggle takes effect"); + CHECK_FALSE(m->getAllowMonospaceLigatures()); + } + + // getStyle reflects the FT face's intrinsic flags. DejaVuSans is + // NORMAL; DejaVuSans-Bold has the BOLD bit set after load. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: getStyle has BOLD for DejaVuSans-Bold and not for DejaVuSans", "[LLFontFreetype]") + { + const std::string normal = std::string(kFontDir) + "DejaVuSans.woff2"; + const std::string bold = std::string(kFontDir) + "DejaVuSans-Bold.woff2"; + if (!fileExists(normal) || !fileExists(bold)) + SKIP("DejaVuSans + DejaVuSans-Bold required"); + + LLPointer n = loadFt(normal); + LLPointer b = loadFt(bold); + UNSCOPED_INFO("normal style has no BOLD bit"); + CHECK((n->getStyle() & LLFontGL::BOLD) == 0); + UNSCOPED_INFO("bold file sets BOLD bit"); + CHECK((b->getStyle() & LLFontGL::BOLD) != 0); + } + + // selectShapingFace walks the fallback chain by codepoint coverage: + // ASCII routes to the head, CJK routes to a CJK fallback, emoji + // routes to the emoji fallback. Pins multi-hop priority resolution + // at llfontfreetype.cpp:266-284. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: selectShapingFace sends ASCII to the head and CJK and emoji to fallbacks", "[LLFontFreetype]") + { + const std::string dejavu = std::string(kFontDir) + "DejaVuSans.woff2"; + const std::string han = std::string(kFontDir) + "SourceHanSans-Regular.woff2"; + const std::string emoji = std::string(kFontDir) + "Noto-COLRv1.ttf"; + if (!fileExists(dejavu) || !fileExists(han) || !fileExists(emoji)) + SKIP("DejaVuSans + SourceHanSans + Noto-COLRv1 required"); + + LLPointer head = loadFt(dejavu); + LLPointer cjk = loadFt(han); + LLPointer emo = loadFt(emoji); + { + INFO("all three loaded"); + REQUIRE(head.notNull()); + REQUIRE(cjk.notNull()); + REQUIRE(emo.notNull()); + } + head->addFallbackFont(cjk); + head->addFallbackFont(emo); + + U32 idx = 0; + // ASCII 'a' lives in the head's cmap → head wins. + UNSCOPED_INFO("'a' resolves to head"); + REQUIRE(head->selectShapingFace(L'a', idx) == head.get()); + UNSCOPED_INFO("head produced a non-zero glyph for 'a'"); + CHECK(idx != 0u); + + // U+4F60 你 lives in SourceHanSans, not in DejaVuSans → CJK fallback. + idx = 0; + UNSCOPED_INFO("CJK codepoint resolves to SourceHanSans fallback"); + REQUIRE(head->selectShapingFace(0x4F60, idx) == cjk.get()); + + // U+1F525 (fire emoji) lives in Noto-COLRv1, not in either text + // face → emoji fallback wins. + idx = 0; + UNSCOPED_INFO("emoji codepoint resolves to Noto-COLRv1 fallback"); + REQUIRE(head->selectShapingFace(0x1F525, idx) == emo.get()); + } + + // mShapingFaceResolution caches selectShapingFace decisions; adding + // a new fallback must invalidate that cache so a previously-resolved + // codepoint can re-route through the newly-attached chain. Pins the + // cache-clear on addFallbackFont at llfontfreetype.cpp:434. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: addFallbackFont clears the faces selectShapingFace has cached", "[LLFontFreetype]") + { + const std::string dejavu = std::string(kFontDir) + "DejaVuSans.woff2"; + const std::string emoji = std::string(kFontDir) + "Noto-COLRv1.ttf"; + if (!fileExists(dejavu) || !fileExists(emoji)) + SKIP("DejaVuSans + Noto-COLRv1 required"); + + LLPointer head = loadFt(dejavu); + LLPointer emo = loadFt(emoji); + { + INFO("both loaded"); + REQUIRE(head.notNull()); + REQUIRE(emo.notNull()); + } + + // First lookup, no fallback registered. 'a' resolves to head; + // emoji resolves to head with idx=0 (no fallback covers it). + U32 idx_a1 = 0, idx_e1 = 0; + const LLFontFreetype* a1 = head->selectShapingFace(L'a', idx_a1); + const LLFontFreetype* e1 = head->selectShapingFace(0x1F525, idx_e1); + UNSCOPED_INFO("'a' first resolution -> head"); + REQUIRE(a1 == head.get()); + UNSCOPED_INFO("emoji first resolution -> head (no fallback)"); + REQUIRE(e1 == head.get()); + UNSCOPED_INFO("emoji on plain head produced glyph 0"); + CHECK(idx_e1 == 0u); + + // Second lookup hits the cache and returns the same pointer. + U32 idx_a2 = 0; + UNSCOPED_INFO("repeat 'a' lookup hits cache"); + REQUIRE(head->selectShapingFace(L'a', idx_a2) == head.get()); + + // Add the emoji fallback. The cache must invalidate so the next + // emoji lookup re-runs the walker through the new chain. + head->addFallbackFont(emo); + + U32 idx_e2 = 0; + UNSCOPED_INFO("after addFallbackFont, emoji re-resolves to fallback"); + REQUIRE(head->selectShapingFace(0x1F525, idx_e2) == emo.get()); + } + + // Functor gating: a fallback whose char_functor returns false for + // codepoint X is excluded from X's resolution, even if its cmap + // covers X. Pins the must-accept gate at llfontfreetype.cpp:268-273. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: a fallback's char functor keeps it from the codepoints it rejects", "[LLFontFreetype]") + { + const std::string dejavu = std::string(kFontDir) + "DejaVuSans.woff2"; + const std::string emoji = std::string(kFontDir) + "TwemojiSVG.woff2"; + if (!fileExists(dejavu) || !fileExists(emoji)) + SKIP("DejaVuSans + TwemojiSVG required"); + + LLPointer head = loadFt(dejavu); + LLPointer emo = loadFt(emoji); + { + INFO("both loaded"); + REQUIRE(head.notNull()); + REQUIRE(emo.notNull()); + } + + // Functor accepts only emoji-range codepoints; rejects ASCII. + // If TwemojiSVG happens to carry ASCII outlines, the functor + // forces the head to win 'a' regardless. + auto pictograph_only = [](llwchar c) -> bool { return c >= 0x1F000; }; + head->addFallbackFont(emo, pictograph_only); + + U32 idx = 0; + UNSCOPED_INFO("'a' resolves to head despite Twemoji ASCII coverage"); + REQUIRE(head->selectShapingFace(L'a', idx) == head.get()); + // Functor allows emoji range, so emoji codepoints route to Twemoji. + idx = 0; + UNSCOPED_INFO("emoji codepoint passes functor and resolves to Twemoji"); + REQUIRE(head->selectShapingFace(0x1F525, idx) == emo.get()); } } diff --git a/indra/llrender/tests/llfontgl_test.cpp b/indra/llrender/tests/llfontgl_test.cpp index b95af706c9..ab4d0b1074 100644 --- a/indra/llrender/tests/llfontgl_test.cpp +++ b/indra/llrender/tests/llfontgl_test.cpp @@ -41,7 +41,7 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include #include @@ -71,8 +71,6 @@ namespace } } -namespace tut -{ namespace { // The shared GL context. TUT spins fixtures per-test method, but @@ -93,21 +91,16 @@ namespace ~llfontgl_data() = default; }; - typedef test_group llfontgl_test; - typedef llfontgl_test::object llfontgl_object; - tut::llfontgl_test llfontgl_testcase("LLFontGL"); -} // initClass through the explicit-path overload should bring up a // working font registry under a real GL context. loadDefaultFonts // (called from initClass) returns true only if every required // family resolved through to a usable LLFontFreetype. - template<> template<> - void llfontgl_object::test<1>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: initClass from an explicit fonts.xml brings up a working registry", "[LLFontGL]") { if (!fileExists(kFontsXml)) { - skip("fonts.xml not found at " + std::string(kFontsXml)); + SKIP("fonts.xml not found at " + std::string(kFontsXml)); return; } @@ -118,22 +111,21 @@ namespace /*font_overrides=*/LLSD(), /*create_gl_textures=*/true); - ensure("default fonts loaded", - LLFontGL::loadDefaultFonts()); - ensure("SansSerif resolves", - LLFontGL::getFontSansSerif() != nullptr); + UNSCOPED_INFO("default fonts loaded"); + CHECK(LLFontGL::loadDefaultFonts()); + UNSCOPED_INFO("SansSerif resolves"); + REQUIRE(LLFontGL::getFontSansSerif() != nullptr); } // After ASCII rasterization, the SansSerif font's bitmap cache // should have at least one greyscale page with a live GL texture. // Catches the silent-no-op failure mode where the GL context is // healthy but createGLTexture skipped the upload. - template<> template<> - void llfontgl_object::test<2>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: ASCII glyphs land on a grayscale atlas page with a live GL texture", "[LLFontGL]") { if (!fileExists(kFontsXml)) { - skip("fonts.xml not found at " + std::string(kFontsXml)); + SKIP("fonts.xml not found at " + std::string(kFontsXml)); return; } @@ -145,7 +137,8 @@ namespace LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("SansSerif resolves", font != nullptr); + UNSCOPED_INFO("SansSerif resolves"); + REQUIRE(font != nullptr); // Force the ASCII range to rasterize so the atlas isn't empty. // (createFont's pre-warm in getFont already does this on first @@ -153,116 +146,125 @@ namespace font->generateASCIIglyphs(); const LLFontFreetype* ft = font->getFontFreetype(); - ensure("LLFontFreetype present", ft != nullptr); + UNSCOPED_INFO("LLFontFreetype present"); + REQUIRE(ft != nullptr); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - ensure("bitmap cache present", cache != nullptr); - ensure("at least one grayscale atlas page", - cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); + UNSCOPED_INFO("bitmap cache present"); + REQUIRE(cache != nullptr); + UNSCOPED_INFO("at least one grayscale atlas page"); + CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); LLImageGL* page = cache->getImageGL(EFontGlyphType::Grayscale, 0); - ensure("page 0 LLImageGL present", page != nullptr); - ensure("page 0 has a live GL texture name", - page->getTexName() != 0); + UNSCOPED_INFO("page 0 LLImageGL present"); + REQUIRE(page != nullptr); + UNSCOPED_INFO("page 0 has a live GL texture name"); + CHECK(page->getTexName() != 0); } // getFontMonospace resolves and reports fixed-width through its // underlying freetype. Pins the registry's monospace family // routing — a regression that resolved Monospace to a // proportional fallback would surface here. - template<> template<> - void llfontgl_object::test<3>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: getFontMonospace resolves to a fixed-width face", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* mono = LLFontGL::getFontMonospace(); - ensure("Monospace resolves", mono != nullptr); + UNSCOPED_INFO("Monospace resolves"); + REQUIRE(mono != nullptr); const LLFontFreetype* ft = mono->getFontFreetype(); - ensure("Monospace has a freetype", ft != nullptr); - ensure("Monospace freetype is fixed-width: " + ft->getName(), - ft->isFixedWidth()); + UNSCOPED_INFO("Monospace has a freetype"); + REQUIRE(ft != nullptr); + UNSCOPED_INFO("Monospace freetype is fixed-width: " + ft->getName()); + CHECK(ft->isFixedWidth()); } // getFontSansSerifBig resolves with positive line-height. // Captures the registry's size-suffix routing — Big size must // produce a freetype whose metrics scale up from the base. - template<> template<> - void llfontgl_object::test<4>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: getFontSansSerifBig has a taller line than getFontSansSerifSmall", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* small = LLFontGL::getFontSansSerifSmall(); LLFontGL* big = LLFontGL::getFontSansSerifBig(); - ensure("Small SansSerif resolves", small != nullptr); - ensure("Big SansSerif resolves", big != nullptr); + UNSCOPED_INFO("Small SansSerif resolves"); + REQUIRE(small != nullptr); + UNSCOPED_INFO("Big SansSerif resolves"); + REQUIRE(big != nullptr); const LLFontFreetype* fs = small->getFontFreetype(); const LLFontFreetype* fb = big->getFontFreetype(); - ensure("Small line-height > 0", fs->getLineHeight() > 0.f); - ensure("Big line-height > 0", fb->getLineHeight() > 0.f); - ensure("Big line-height > Small line-height", - fb->getLineHeight() > fs->getLineHeight()); + UNSCOPED_INFO("Small line-height > 0"); + CHECK(fs->getLineHeight() > 0.f); + UNSCOPED_INFO("Big line-height > 0"); + CHECK(fb->getLineHeight() > 0.f); + UNSCOPED_INFO("Big line-height > Small line-height"); + CHECK(fb->getLineHeight() > fs->getLineHeight()); } // Repeat-call getter caches: getFontSansSerif() returns the same // pointer on second call. Pins the per-getter static cache — // a regression that re-resolved on every call would change the // pointer (and waste a registry lookup per UI label render). - template<> template<> - void llfontgl_object::test<5>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: getFontSansSerif returns the same font on every call", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* a = LLFontGL::getFontSansSerif(); LLFontGL* b = LLFontGL::getFontSansSerif(); - ensure("first resolve non-null", a != nullptr); - ensure_equals("second resolve returns the same instance", a, b); + UNSCOPED_INFO("first resolve non-null"); + REQUIRE(a != nullptr); + UNSCOPED_INFO("second resolve returns the same instance"); + REQUIRE(a == b); } // generateASCIIglyphs is idempotent — repeat calls don't allocate // additional atlas sheets. Pins the head-cache lookup that makes // the second call a fast no-op. - template<> template<> - void llfontgl_object::test<6>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: a second generateASCIIglyphs adds no atlas sheets", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); font->generateASCIIglyphs(); const LLFontFreetype* ft = font->getFontFreetype(); const LLFontBitmapCache* bc = ft->getFontBitmapCache(); const U32 sheets_after_first = bc->getNumBitmaps(EFontGlyphType::Grayscale); - ensure("at least one sheet after first generate", - sheets_after_first >= 1u); + UNSCOPED_INFO("at least one sheet after first generate"); + CHECK(sheets_after_first >= 1u); font->generateASCIIglyphs(); const U32 sheets_after_second = bc->getNumBitmaps(EFontGlyphType::Grayscale); - ensure_equals("second generate is idempotent (no extra sheets)", - sheets_after_second, sheets_after_first); + UNSCOPED_INFO("second generate is idempotent (no extra sheets)"); + CHECK(sheets_after_second == sheets_after_first); } // getFontByName returns the same instance for the same legacy // name string. Pins the legacy-name routing in the registry. - template<> template<> - void llfontgl_object::test<7>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: getFontByName returns the same instance for the same legacy name", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* a = LLFontGL::getFontByName("SANSSERIF"); LLFontGL* b = LLFontGL::getFontByName("SANSSERIF"); - ensure("legacy name resolves to non-null", a != nullptr); - ensure_equals("repeat lookup returns same instance", a, b); + UNSCOPED_INFO("legacy name resolves to non-null"); + REQUIRE(a != nullptr); + UNSCOPED_INFO("repeat lookup returns same instance"); + REQUIRE(a == b); } // collectGarbage on the font face does not crash and (since @@ -270,17 +272,18 @@ namespace // are recently used. Pins 169390b593 (cache-sweep moved off // hot path) and 7c05b2de82 (collectGarbage safe at any frame // boundary). - template<> template<> - void llfontgl_object::test<8>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: collectGarbage leaves glyphs in use and their atlas page alone", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); const LLFontFreetype* ft = font->getFontFreetype(); - ensure("freetype present", ft != nullptr); + UNSCOPED_INFO("freetype present"); + REQUIRE(ft != nullptr); // What a sweep must not do is disturb glyphs still in use. Measure // first, sweep twice -- the second is inside the throttle interval and @@ -291,37 +294,41 @@ namespace const std::string sample = "The quick brown fox"; font->generateASCIIglyphs(); const F32 before = font->getWidthF32Bytes(sample, 0, (S32)sample.size(), false); - ensure("sample has a width to begin with", before > 0.f); + UNSCOPED_INFO("sample has a width to begin with"); + CHECK(before > 0.f); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - ensure("bitmap cache present", cache != nullptr); + UNSCOPED_INFO("bitmap cache present"); + REQUIRE(cache != nullptr); const U32 pages_before = cache->getNumBitmaps(EFontGlyphType::Grayscale); ft->collectGarbage(); ft->collectGarbage(); const F32 after = font->getWidthF32Bytes(sample, 0, (S32)sample.size(), false); - ensure_equals("a sweep does not change what live text measures", - before, after); - ensure_equals("a sweep does not drop a page holding live glyphs", - cache->getNumBitmaps(EFontGlyphType::Grayscale), pages_before); + UNSCOPED_INFO("a sweep does not change what live text measures"); + CHECK(before == after); + UNSCOPED_INFO("a sweep does not drop a page holding live glyphs"); + CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) == pages_before); LLImageGL* page = cache->getImageGL(EFontGlyphType::Grayscale, 0); - ensure("page 0 still present after a sweep", page != nullptr); - ensure("page 0 still has a live GL texture", page->getTexName() != 0); + UNSCOPED_INFO("page 0 still present after a sweep"); + REQUIRE(page != nullptr); + UNSCOPED_INFO("page 0 still has a live GL texture"); + CHECK(page->getTexName() != 0); } // Re-calling initClass after the registry is up is a no-op // (warns + bails). The registry pointer must remain stable so // LLFontGL's static fontp caches don't dangle. - template<> template<> - void llfontgl_object::test<9>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: a second initClass leaves the fonts already resolved in place", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* before = LLFontGL::getFontSansSerif(); - ensure("first resolve non-null", before != nullptr); + UNSCOPED_INFO("first resolve non-null"); + REQUIRE(before != nullptr); // Re-call initClass with different DPI. The single-shot // guard short-circuits, so the registry stays the same and @@ -329,87 +336,125 @@ namespace LLFontGL::initClass(72.f, 0.5f, 0.5f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* after = LLFontGL::getFontSansSerif(); - ensure_equals("second resolve returns same instance after re-init", - after, before); + UNSCOPED_INFO("second resolve returns same instance after re-init"); + REQUIRE(after == before); + } + + // Static getter family — every named getter must resolve to a + // non-null LLFontGL after a successful initClass. Smoke check + // that the default fonts.xml covers all the families exposed. + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: every named font getter resolves after initClass", "[LLFontGL]") + { + if (!fileExists(kFontsXml)) + SKIP("fonts.xml not found"); + LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, + LLSD(), /*create_gl_textures=*/true); + UNSCOPED_INFO("SansSerif"); + REQUIRE(LLFontGL::getFontSansSerif() != nullptr); + UNSCOPED_INFO("SansSerifSmall"); + REQUIRE(LLFontGL::getFontSansSerifSmall() != nullptr); + UNSCOPED_INFO("SansSerifSmallBold"); + REQUIRE(LLFontGL::getFontSansSerifSmallBold() != nullptr); + UNSCOPED_INFO("SansSerifBig"); + REQUIRE(LLFontGL::getFontSansSerifBig() != nullptr); + UNSCOPED_INFO("SansSerifHuge"); + REQUIRE(LLFontGL::getFontSansSerifHuge() != nullptr); + UNSCOPED_INFO("SansSerifBold"); + REQUIRE(LLFontGL::getFontSansSerifBold() != nullptr); + UNSCOPED_INFO("Monospace"); + REQUIRE(LLFontGL::getFontMonospace() != nullptr); + UNSCOPED_INFO("EmojiSmall"); + REQUIRE(LLFontGL::getFontEmojiSmall() != nullptr); + UNSCOPED_INFO("EmojiMedium"); + REQUIRE(LLFontGL::getFontEmojiMedium() != nullptr); + UNSCOPED_INFO("EmojiLarge"); + REQUIRE(LLFontGL::getFontEmojiLarge() != nullptr); + UNSCOPED_INFO("FontDefault"); + REQUIRE(LLFontGL::getFontDefault() != nullptr); } // getWidthF32: the whole-string overload defaults to the text's length; // a getWidthF32Bytes call with an explicit budget matches it. - template<> template<> - void llfontgl_object::test<11>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: getWidthF32 of a whole string equals getWidthF32Bytes of all of it", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); const std::string s = "Hello"; const F32 wfull = font->getWidthF32(s); const F32 wexp = font->getWidthF32Bytes(s, 0, (S32)s.size(), false); - ensure("getWidthF32 with default max == explicit max", - wfull == wexp); - ensure("width is positive", wfull > 0.f); + UNSCOPED_INFO("getWidthF32 with default max == explicit max"); + CHECK(wfull == wexp); + UNSCOPED_INFO("width is positive"); + CHECK(wfull > 0.f); } // Width monotonicity: width(AAA) > width(AA) > width(A). Used by // render()'s halign math at llfontgl.cpp:354-372 — the alignment // calc relies on accumulated width strictly increasing with // character count for the same string. - template<> template<> - void llfontgl_object::test<12>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: a string grows wider with each character added", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); const F32 w1 = font->getWidthF32(std::string("A")); const F32 w2 = font->getWidthF32(std::string("AA")); const F32 w3 = font->getWidthF32(std::string("AAA")); - ensure("width(A) > 0", w1 > 0.f); - ensure("width(AA) > width(A)", w2 > w1); - ensure("width(AAA) > width(AA)", w3 > w2); + UNSCOPED_INFO("width(A) > 0"); + CHECK(w1 > 0.f); + UNSCOPED_INFO("width(AA) > width(A)"); + CHECK(w2 > w1); + UNSCOPED_INFO("width(AAA) > width(AA)"); + CHECK(w3 > w2); } // Ellipsis budget: getWidthF32("....") is positive — render() pre- // computes this for the truncation path (llfontgl.cpp:459 references // the dots constant). A regression where '.' rasterized to a 0-width // glyph would silently break ellipsis sizing. - template<> template<> - void llfontgl_object::test<13>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: getWidthF32 gives the ellipsis dots a positive width", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); const F32 w = font->getWidthF32(std::string("....")); - ensure("ellipsis width is positive", w > 0.f); + UNSCOPED_INFO("ellipsis width is positive"); + CHECK(w > 0.f); } // maxDrawableBytes boundary: zero pixel budget fits nothing, // F32_MAX fits all, mid-budget fits a partial prefix. - template<> template<> - void llfontgl_object::test<14>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: maxDrawableBytes fits none or all or the prefix the budget holds", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); const std::string s = "Hello"; - ensure_equals("budget 0 fits nothing", - font->maxDrawableBytes(s, 0.f, (S32)s.size()), + UNSCOPED_INFO("budget 0 fits nothing"); + REQUIRE(font->maxDrawableBytes(s, 0.f, (S32)s.size()) == (S32)0); - ensure_equals("budget F32_MAX fits the whole string", - font->maxDrawableBytes(s, F32_MAX, - (S32)s.size()), + UNSCOPED_INFO("budget F32_MAX fits the whole string"); + REQUIRE(font->maxDrawableBytes(s, F32_MAX, + (S32)s.size()) == (S32)s.size()); // Find a budget that should fit "Hel" but not "Hell". const F32 w_hel = font->getWidthF32Bytes(s, 0, 3, false); @@ -418,22 +463,22 @@ namespace const F32 mid_budget = (w_hel + w_hell) * 0.5f; const S32 fit = font->maxDrawableBytes(s, mid_budget, (S32)s.size()); - ensure("partial-budget fit is in [3, 3] (Hel exactly)", - fit == 3); + UNSCOPED_INFO("partial-budget fit is in [3, 3] (Hel exactly)"); + CHECK(fit == 3); } // byteFromPixelOffset round-trip: for each prefix length i, the // pixel offset corresponding to the prefix's width must invert to // i. Width accumulation and pixel->offset lookup must agree. - template<> template<> - void llfontgl_object::test<15>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: byteFromPixelOffset inverts getWidthF32Bytes at every character start", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); const std::string s = "Hello"; // ASCII, so one byte per character: prefix lengths 0..5. for (S32 i = 0; i <= (S32)s.size(); ++i) @@ -448,8 +493,8 @@ namespace // character wide -- it accepts precisely the mistake this is here // to catch -- and the target is a boundary, which rounding takes // to the boundary itself. - ensure_equals("round-trip lands on the offset it was measured from", - cp, i); + UNSCOPED_INFO("round-trip lands on the offset it was measured from"); + REQUIRE(cp == i); } // The same round trip where a character is not a byte. The expected @@ -461,7 +506,8 @@ namespace const F32 w = font->getWidthF32Bytes(m, 0, start, false); const S32 got = font->byteFromPixelOffset(m, 0, w, F32_MAX, (S32)m.size(), /*round=*/true); - ensure_equals("round-trip through a multi-byte character", got, start); + UNSCOPED_INFO("round-trip through a multi-byte character"); + REQUIRE(got == start); } } @@ -470,15 +516,15 @@ namespace // width measurement boundary — a regression that shaped "a你" // against DejaVu only (replacing 你 with notdef) would diverge. // Skip if the default fallback chain doesn't cover U+4F60. - template<> template<> - void llfontgl_object::test<16>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: a mixed-script string is as wide as its parts", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); const std::string a_s = "a"; const std::string c_s = utf8str_from_cp(0x4F60); @@ -488,11 +534,56 @@ namespace const F32 wc = font->getWidthF32(c_s); const F32 wac = font->getWidthF32(ac_s); if (wc == 0.f) - skip("font's fallback chain doesn't cover U+4F60"); + SKIP("font's fallback chain doesn't cover U+4F60"); // Tolerance: ~0.5 px accounts for sub-pixel positioning across // shape-range boundaries. - ensure("width(a你) ≈ width(a) + width(你) within 0.5px", - std::abs(wac - (wa + wc)) <= 0.5f); + UNSCOPED_INFO("width(a你) ≈ width(a) + width(你) within 0.5px"); + CHECK(std::abs(wac - (wa + wc)) <= 0.5f); + } + + // Style string parser round-trip: every output of getStringFromStyle + // must round-trip through getStyleFromString. The legacy parser used + // substring search, so a poisoned token like "FAUX-BOLD" set BOLD; + // the new tokenizer-based parser splits on '|' and matches whole + // tokens. NORMAL has its own dedicated string ("NORMAL", no leading + // pipe), so the round-trip pins both edge cases at once. + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: getStringFromStyle round-trips and the parser matches whole tokens", "[LLFontGL]") + { + const U8 cases[] = { + LLFontGL::NORMAL, + LLFontGL::BOLD, + LLFontGL::ITALIC, + LLFontGL::UNDERLINE, + LLFontGL::BOLD | LLFontGL::ITALIC, + LLFontGL::BOLD | LLFontGL::UNDERLINE, + LLFontGL::ITALIC | LLFontGL::UNDERLINE, + LLFontGL::BOLD | LLFontGL::ITALIC | LLFontGL::UNDERLINE, + }; + for (U8 style : cases) + { + const std::string s = LLFontGL::getStringFromStyle(style); + UNSCOPED_INFO("string output is non-empty"); + REQUIRE_FALSE(s.empty()); + UNSCOPED_INFO("no leading pipe"); + REQUIRE(s.front() != '|'); + UNSCOPED_INFO("no trailing pipe"); + REQUIRE(s.back() != '|'); + UNSCOPED_INFO("round-trip preserves style bits"); + REQUIRE(LLFontGL::getStyleFromString(s) == style); + } + + // Substring-match regression guard: legacy parser matched + // "BOLD" inside "FAUX-BOLD"; new parser tokenizes on '|'. + UNSCOPED_INFO("FAUX-BOLD does not set BOLD"); + CHECK(LLFontGL::getStyleFromString("FAUX-BOLD") == 0); + UNSCOPED_INFO("NOTBOLD does not set BOLD"); + CHECK(LLFontGL::getStyleFromString("NOTBOLD") == 0); + UNSCOPED_INFO("empty string is NORMAL"); + CHECK(LLFontGL::getStyleFromString("") == + LLFontGL::NORMAL); + UNSCOPED_INFO("BOLD|ITALIC tokenizes correctly"); + CHECK(LLFontGL::getStyleFromString("BOLD|ITALIC") == + (LLFontGL::BOLD | LLFontGL::ITALIC)); } // byteFromPixelOffset must snap to cluster boundaries on a @@ -504,15 +595,15 @@ namespace // selection rect's right edge across the cluster's interior — // observed as drag-select highlight flicker on ZWJ flags / family // emoji where 🐶 (1 codepoint) is unaffected. - template<> template<> - void llfontgl_object::test<18>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: byteFromPixelOffset never lands inside a ZWJ emoji cluster", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); // "X" + trans flag (5 codepoints) + "Y". The cluster's byte bounds // are recorded as the string is built rather than counted by hand: @@ -531,7 +622,7 @@ namespace const F32 wfull = font->getWidthF32Bytes(s, 0, total, false); if (wfull <= 0.f) - skip("emoji fallback not available"); + SKIP("emoji fallback not available"); // Sanity: width up to the cluster's end must exceed width up to its // start by at least a few pixels — otherwise the emoji didn't @@ -540,7 +631,7 @@ namespace const F32 w_pre = font->getWidthF32Bytes(s, 0, cluster_start, false); const F32 w_post = font->getWidthF32Bytes(s, 0, cluster_end, false); if (w_post - w_pre < 2.f) - skip("emoji cluster did not shape to a measurable glyph"); + SKIP("emoji cluster did not shape to a measurable glyph"); // Walk target_x across the rendered cluster's pixel span at // sub-pixel granularity. Every returned cursor pos must land on a @@ -554,75 +645,9 @@ namespace x, F32_MAX, total, /*round=*/true); const bool inside_cluster = (pos > cluster_start && pos < cluster_end); - ensure("byteFromPixelOffset round=true returns mid-cluster offset", - !inside_cluster); - } - } - - // Static getter family — every named getter must resolve to a - // non-null LLFontGL after a successful initClass. Smoke check - // that the default fonts.xml covers all the families exposed. - template<> template<> - void llfontgl_object::test<10>() - { - if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); - LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, - LLSD(), /*create_gl_textures=*/true); - ensure("SansSerif", LLFontGL::getFontSansSerif() != nullptr); - ensure("SansSerifSmall", LLFontGL::getFontSansSerifSmall() != nullptr); - ensure("SansSerifSmallBold", LLFontGL::getFontSansSerifSmallBold() != nullptr); - ensure("SansSerifBig", LLFontGL::getFontSansSerifBig() != nullptr); - ensure("SansSerifHuge", LLFontGL::getFontSansSerifHuge() != nullptr); - ensure("SansSerifBold", LLFontGL::getFontSansSerifBold() != nullptr); - ensure("Monospace", LLFontGL::getFontMonospace() != nullptr); - ensure("EmojiSmall", LLFontGL::getFontEmojiSmall() != nullptr); - ensure("EmojiMedium", LLFontGL::getFontEmojiMedium() != nullptr); - ensure("EmojiLarge", LLFontGL::getFontEmojiLarge() != nullptr); - ensure("FontDefault", LLFontGL::getFontDefault() != nullptr); - } - - // Style string parser round-trip: every output of getStringFromStyle - // must round-trip through getStyleFromString. The legacy parser used - // substring search, so a poisoned token like "FAUX-BOLD" set BOLD; - // the new tokenizer-based parser splits on '|' and matches whole - // tokens. NORMAL has its own dedicated string ("NORMAL", no leading - // pipe), so the round-trip pins both edge cases at once. - template<> template<> - void llfontgl_object::test<17>() - { - const U8 cases[] = { - LLFontGL::NORMAL, - LLFontGL::BOLD, - LLFontGL::ITALIC, - LLFontGL::UNDERLINE, - LLFontGL::BOLD | LLFontGL::ITALIC, - LLFontGL::BOLD | LLFontGL::UNDERLINE, - LLFontGL::ITALIC | LLFontGL::UNDERLINE, - LLFontGL::BOLD | LLFontGL::ITALIC | LLFontGL::UNDERLINE, - }; - for (U8 style : cases) - { - const std::string s = LLFontGL::getStringFromStyle(style); - ensure("string output is non-empty", !s.empty()); - ensure("no leading pipe", s.front() != '|'); - ensure("no trailing pipe", s.back() != '|'); - ensure_equals("round-trip preserves style bits", - LLFontGL::getStyleFromString(s), style); + UNSCOPED_INFO("byteFromPixelOffset round=true returns mid-cluster offset"); + REQUIRE_FALSE(inside_cluster); } - - // Substring-match regression guard: legacy parser matched - // "BOLD" inside "FAUX-BOLD"; new parser tokenizes on '|'. - ensure_equals("FAUX-BOLD does not set BOLD", - LLFontGL::getStyleFromString("FAUX-BOLD"), 0); - ensure_equals("NOTBOLD does not set BOLD", - LLFontGL::getStyleFromString("NOTBOLD"), 0); - ensure_equals("empty string is NORMAL", - LLFontGL::getStyleFromString(""), - LLFontGL::NORMAL); - ensure_equals("BOLD|ITALIC tokenizes correctly", - LLFontGL::getStyleFromString("BOLD|ITALIC"), - LLFontGL::BOLD | LLFontGL::ITALIC); } // firstDrawableByte is the only one of the three layout walks that goes @@ -632,15 +657,15 @@ namespace // width. Sweep the budget across a string with a ZWJ cluster in it and // hold every answer to the promise llfontgl.h makes -- a character start, // and where the text shapes, a cluster start. - template<> template<> - void llfontgl_object::test<19>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: firstDrawableByte never lands inside a character or a ZWJ cluster", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); // Byte bounds recorded as the string is built: the five codepoints of // a trans flag are 4/3/3/3/3 bytes, so a hand-written offset here @@ -658,22 +683,25 @@ namespace const F32 wfull = font->getWidthF32Bytes(s, 0, total, false); if (wfull <= 0.f) - skip("emoji fallback not available"); + SKIP("emoji fallback not available"); const F32 w_pre = font->getWidthF32Bytes(s, 0, cluster_start, false); const F32 w_post = font->getWidthF32Bytes(s, 0, cluster_end, false); if (w_post - w_pre < 2.f) - skip("emoji cluster did not shape to a measurable glyph"); + SKIP("emoji cluster did not shape to a measurable glyph"); for (F32 px = 0.f; px <= wfull + 2.f; px += 0.5f) { const S32 first = font->firstDrawableByte(s, px); - ensure("firstDrawableByte names a position inside the text", - first >= 0 && first < total); - ensure("firstDrawableByte returns a continuation byte", - ((unsigned char)s[first] & 0xC0) != 0x80); + { + INFO("firstDrawableByte names a position inside the text"); + REQUIRE(first >= 0); + REQUIRE(first < total); + } + UNSCOPED_INFO("firstDrawableByte returns a continuation byte"); + REQUIRE(((unsigned char)s[first] & 0xC0) != 0x80); const bool inside_cluster = (first > cluster_start && first < cluster_end); - ensure("firstDrawableByte returns a position inside a cluster", - !inside_cluster); + UNSCOPED_INFO("firstDrawableByte returns a position inside a cluster"); + REQUIRE_FALSE(inside_cluster); } } @@ -681,180 +709,184 @@ namespace // set of positions a full sweep can return has to be exactly the set of // character starts: anything missing is a character the backward walk // stepped over, and anything extra is an offset inside one. - template<> template<> - void llfontgl_object::test<20>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: firstDrawableByte sweeps through every character start and no other", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); // "a", e-acute (two bytes), "b", "c" -- so byte 2 is the only offset // in the string that is not somewhere drawing may begin. const std::string s = "a\xC3\xA9" "bc"; const S32 total = (S32)s.size(); - ensure_equals("the fixture string is five bytes", total, (S32)5); + UNSCOPED_INFO("the fixture string is five bytes"); + CHECK(total == (S32)5); const F32 wfull = font->getWidthF32Bytes(s, 0, total, false); if (wfull <= 0.f) - skip("font produced no width"); + SKIP("font produced no width"); std::set seen; S32 previous = total; for (F32 px = 0.f; px <= wfull + 2.f; px += 0.25f) { const S32 first = font->firstDrawableByte(s, px); - ensure("a larger budget starts drawing later", first <= previous); + UNSCOPED_INFO("a larger budget starts drawing later"); + REQUIRE(first <= previous); previous = first; seen.insert(first); } const std::set starts{ 0, 1, 3, 4 }; - ensure("the sweep reaches every character start and nothing else", - seen == starts); + UNSCOPED_INFO("the sweep reaches every character start and nothing else"); + CHECK(seen == starts); } // The edges, and the one case with no good answer. A budget that cannot // hold even the last character still has to name a place to draw from -- // that character, clipped -- because returning nothing would leave a // caller scrolling a field it can never make progress in. - template<> template<> - void llfontgl_object::test<21>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: firstDrawableByte on empty text and at zero and unlimited budgets", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); const std::string s = "Hello"; const S32 total = (S32)s.size(); const F32 wfull = font->getWidthF32Bytes(s, 0, total, false); if (wfull <= 0.f) - skip("font produced no width"); + SKIP("font produced no width"); - ensure_equals("empty text draws from the beginning", - font->firstDrawableByte(std::string_view(), 100.f), (S32)0); - ensure_equals("room for everything draws from the beginning", - font->firstDrawableByte(s, F32_MAX), (S32)0); - ensure_equals("no room at all still draws the last character", - font->firstDrawableByte(s, 0.f), total - 1); - ensure_equals("a byte budget of nothing draws from the beginning", - font->firstDrawableByte(s, F32_MAX, S32_MAX, 0), (S32)0); + UNSCOPED_INFO("empty text draws from the beginning"); + CHECK(font->firstDrawableByte(std::string_view(), 100.f) == (S32)0); + UNSCOPED_INFO("room for everything draws from the beginning"); + CHECK(font->firstDrawableByte(s, F32_MAX) == (S32)0); + UNSCOPED_INFO("no room at all still draws the last character"); + CHECK(font->firstDrawableByte(s, 0.f) == total - 1); + UNSCOPED_INFO("a byte budget of nothing draws from the beginning"); + CHECK(font->firstDrawableByte(s, F32_MAX, S32_MAX, 0) == (S32)0); // start_pos chooses which character has to be the last one drawn, so // with room to spare the answer is the beginning whichever it is. for (S32 start = 0; start < total; ++start) { - ensure_equals("room for everything draws from the beginning", - font->firstDrawableByte(s, F32_MAX, start), (S32)0); - ensure_equals("no room draws the character asked for", - font->firstDrawableByte(s, 0.f, start), start); + UNSCOPED_INFO("room for everything draws from the beginning"); + REQUIRE(font->firstDrawableByte(s, F32_MAX, start) == (S32)0); + UNSCOPED_INFO("no room draws the character asked for"); + REQUIRE(font->firstDrawableByte(s, 0.f, start) == start); } } // maxDrawableBytes over text where a character is not a byte. The ASCII // test beside this one cannot tell a byte count from a character count, a // glyph count or a cluster count -- they are the same number there. - template<> template<> - void llfontgl_object::test<22>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: maxDrawableBytes over text where a character is not a byte", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); // Three characters, three bytes each. const std::string s = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E"; const S32 total = (S32)s.size(); - ensure_equals("the fixture string is nine bytes", total, (S32)9); + UNSCOPED_INFO("the fixture string is nine bytes"); + CHECK(total == (S32)9); if (font->getWidthF32Bytes(s, 0, total, false) <= 0.f) - skip("font produced no width for the sample"); + SKIP("font produced no width for the sample"); - ensure_equals("all the room in the world fits every byte", - font->maxDrawableBytes(s, F32_MAX, total), total); - ensure_equals("no room fits nothing", - font->maxDrawableBytes(s, 0.f, total), (S32)0); + UNSCOPED_INFO("all the room in the world fits every byte"); + CHECK(font->maxDrawableBytes(s, F32_MAX, total) == total); + UNSCOPED_INFO("no room fits nothing"); + CHECK(font->maxDrawableBytes(s, 0.f, total) == (S32)0); // A budget that stops inside a character has to come back to where // that character started. Answering 4 would split it. - ensure_equals("a byte budget inside a character backs off", - font->maxDrawableBytes(s, F32_MAX, 4), (S32)3); - ensure_equals("and again one byte further in", - font->maxDrawableBytes(s, F32_MAX, 5), (S32)3); - ensure_equals("a budget on a boundary is kept", - font->maxDrawableBytes(s, F32_MAX, 6), (S32)6); + UNSCOPED_INFO("a byte budget inside a character backs off"); + CHECK(font->maxDrawableBytes(s, F32_MAX, 4) == (S32)3); + UNSCOPED_INFO("and again one byte further in"); + CHECK(font->maxDrawableBytes(s, F32_MAX, 5) == (S32)3); + UNSCOPED_INFO("a budget on a boundary is kept"); + CHECK(font->maxDrawableBytes(s, F32_MAX, 6) == (S32)6); // By pixels rather than bytes: room for two characters and not three. const F32 w2 = font->getWidthF32Bytes(s, 0, 6, false); const F32 w3 = font->getWidthF32Bytes(s, 0, 9, false); - ensure("the third character has a width", w3 > w2); - ensure_equals("a pixel budget also lands on a character", - font->maxDrawableBytes(s, (w2 + w3) * 0.5f, total), (S32)6); + UNSCOPED_INFO("the third character has a width"); + CHECK(w3 > w2); + UNSCOPED_INFO("a pixel budget also lands on a character"); + CHECK(font->maxDrawableBytes(s, (w2 + w3) * 0.5f, total) == (S32)6); } // begin_offset, and what the answer is counted from. Every one of these // takes an offset to start at, and byteFromPixelOffset reports back // relative to it -- LLLineEditor::calcCursorPos adds its scroll position // to the result, so an absolute answer would double it. - template<> template<> - void llfontgl_object::test<23>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: measuring and hit-testing from begin_offset answer relative to it", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); // "A", a three-byte character, "B" -- and the tail on its own. const std::string s = "A\xE6\x97\xA5" "B"; const std::string tail = "\xE6\x97\xA5" "B"; if (font->getWidthF32Bytes(s, 0, (S32)s.size(), false) <= 0.f) - skip("font produced no width for the sample"); + SKIP("font produced no width for the sample"); - ensure_equals("measuring from an offset measures the rest", - font->getWidthF32Bytes(s, 1, S32_MAX, false), + UNSCOPED_INFO("measuring from an offset measures the rest"); + CHECK(font->getWidthF32Bytes(s, 1, S32_MAX, false) == font->getWidthF32Bytes(tail, 0, S32_MAX, false)); - ensure_equals("S32_MAX from a non-zero offset does not overflow", - font->getWidthF32Bytes(s, 1, S32_MAX, false), + UNSCOPED_INFO("S32_MAX from a non-zero offset does not overflow"); + REQUIRE(font->getWidthF32Bytes(s, 1, S32_MAX, false) == font->getWidthF32Bytes(s, 1, (S32)s.size() - 1, false)); - ensure_equals("an offset at the end measures nothing", - font->getWidthF32Bytes(s, (S32)s.size(), S32_MAX, false), 0.f); + UNSCOPED_INFO("an offset at the end measures nothing"); + REQUIRE(font->getWidthF32Bytes(s, (S32)s.size(), S32_MAX, false) == 0.f); // Hit-testing from an offset answers in the same frame of reference. const F32 w_tail_first = font->getWidthF32Bytes(tail, 0, 3, false); - ensure_equals("hit-testing from an offset is relative to it", - font->byteFromPixelOffset(s, 1, w_tail_first, F32_MAX, - S32_MAX, /*round=*/true), + UNSCOPED_INFO("hit-testing from an offset is relative to it"); + CHECK(font->byteFromPixelOffset(s, 1, w_tail_first, F32_MAX, + S32_MAX, /*round=*/true) == (S32)3); // round=false asks which character the pixel is inside, rather than // which boundary it is nearest, so a point just past a character's // start still belongs to that character. const F32 just_inside = font->getWidthF32Bytes(s, 0, 1, false) + 0.5f; - ensure_equals("round=false stays in the character the pixel is in", - font->byteFromPixelOffset(s, 0, just_inside, F32_MAX, - S32_MAX, /*round=*/false), + UNSCOPED_INFO("round=false stays in the character the pixel is in"); + CHECK(font->byteFromPixelOffset(s, 0, just_inside, F32_MAX, + S32_MAX, /*round=*/false) == (S32)1); } // The two wrap styles that are not ANYWHERE. maxDrawableBytes was rewritten // onto UAX #14 in this work and only its default was ever exercised. - template<> template<> - void llfontgl_object::test<24>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: the word-boundary wrap styles retreat to a break", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); const std::string s = "alpha beta"; const S32 total = (S32)s.size(); @@ -864,19 +896,20 @@ namespace // wherever it runs out; the word-boundary styles retreat to the break. const F32 w_partial = font->getWidthF32Bytes(s, 0, 8, false); if (w_partial <= 0.f) - skip("font produced no width for the sample"); + SKIP("font produced no width for the sample"); const S32 anywhere = font->maxDrawableBytes(s, w_partial, total, LLFontGL::ANYWHERE); - ensure("ANYWHERE cuts inside the last word", anywhere > after_space); + UNSCOPED_INFO("ANYWHERE cuts inside the last word"); + CHECK(anywhere > after_space); - ensure_equals("a word boundary is preferred when there is one", - font->maxDrawableBytes(s, w_partial, total, - LLFontGL::WORD_BOUNDARY_IF_POSSIBLE), + UNSCOPED_INFO("a word boundary is preferred when there is one"); + CHECK(font->maxDrawableBytes(s, w_partial, total, + LLFontGL::WORD_BOUNDARY_IF_POSSIBLE) == after_space); - ensure_equals("and required when the style says only", - font->maxDrawableBytes(s, w_partial, total, - LLFontGL::ONLY_WORD_BOUNDARIES), + UNSCOPED_INFO("and required when the style says only"); + CHECK(font->maxDrawableBytes(s, w_partial, total, + LLFontGL::ONLY_WORD_BOUNDARIES) == after_space); // One long word offers no break at all. IF_POSSIBLE has to give the @@ -887,8 +920,8 @@ namespace const S32 no_break = font->maxDrawableBytes(one_word, w_some, (S32)one_word.size(), LLFontGL::WORD_BOUNDARY_IF_POSSIBLE); - ensure("with no legal break the caller still gets progress", - no_break > 0); + UNSCOPED_INFO("with no legal break the caller still gets progress"); + CHECK(no_break > 0); } // maxDrawableBytes shapes a window sized from the pixel budget rather than @@ -902,15 +935,15 @@ namespace // them proves nothing. What proves it is that a small budget over a long // string -- where the window is a fraction of the text -- agrees with the // same question asked over a string short enough that no window applies. - template<> template<> - void llfontgl_object::test<25>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: maxDrawableBytes on long text fits what getWidthF32Bytes says fits", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); // Long enough that a small pixel budget windows well inside it, in a // mix of scripts so the shaping is not trivially uniform. @@ -922,9 +955,10 @@ namespace s += "caf\xC3\xA9 na\xC3\xAFve "; } const S32 total = (S32)s.size(); - ensure("the fixture string is long", total > 1500); + UNSCOPED_INFO("the fixture string is long"); + CHECK(total > 1500); if (font->getWidthF32Bytes(s, 0, 64, false) <= 0.f) - skip("font produced no width for the sample"); + SKIP("font produced no width for the sample"); // Held against getWidthF32Bytes rather than against maxDrawableBytes // itself. Asking the same function a second question only proves it is @@ -936,19 +970,23 @@ namespace for (F32 px = 8.f; px < 900.f; px += 13.f) { const S32 got = font->maxDrawableBytes(s, px, total, LLFontGL::ANYWHERE); - ensure("the answer stays inside the text", got >= 0 && got <= total); + { + INFO("the answer stays inside the text"); + REQUIRE(got >= 0); + REQUIRE(got <= total); + } if (got > 0) { - ensure("what fits is within the budget", - font->getWidthF32Bytes(s, 0, got, false) <= px); + UNSCOPED_INFO("what fits is within the budget"); + REQUIRE(font->getWidthF32Bytes(s, 0, got, false) <= px); } if (got < total) { // And it is the most that fits: one character more overruns. const S32 more = (S32)utf8str_step_grapheme_forward(s, (size_t)got); - ensure("and one character more does not", - font->getWidthF32Bytes(s, 0, more, false) > px); + UNSCOPED_INFO("and one character more does not"); + REQUIRE(font->getWidthF32Bytes(s, 0, more, false) > px); } } @@ -960,20 +998,23 @@ namespace for (F32 px = 24.f; px < 900.f; px += 37.f) { const S32 got = font->maxDrawableBytes(s, px, total, style); - ensure("a word-boundary answer stays inside the text", - got >= 0 && got <= total); - ensure("a word-boundary answer never overruns", - got == 0 || font->getWidthF32Bytes(s, 0, got, false) <= px); - ensure("and never exceeds what ANYWHERE would take", - got <= font->maxDrawableBytes(s, px, total, LLFontGL::ANYWHERE)); + { + INFO("a word-boundary answer stays inside the text"); + REQUIRE(got >= 0); + REQUIRE(got <= total); + } + UNSCOPED_INFO("a word-boundary answer never overruns"); + REQUIRE((got == 0 || font->getWidthF32Bytes(s, 0, got, false) <= px)); + UNSCOPED_INFO("and never exceeds what ANYWHERE would take"); + REQUIRE(got <= font->maxDrawableBytes(s, px, total, LLFontGL::ANYWHERE)); } } // A budget nothing fits in, and a budget everything fits in. - ensure_equals("no room fits nothing", - font->maxDrawableBytes(s, 0.f, total), (S32)0); - ensure_equals("all the room takes everything", - font->maxDrawableBytes(s, F32_MAX, total), total); + UNSCOPED_INFO("no room fits nothing"); + CHECK(font->maxDrawableBytes(s, 0.f, total) == (S32)0); + UNSCOPED_INFO("all the room takes everything"); + CHECK(font->maxDrawableBytes(s, F32_MAX, total) == total); } @@ -985,8 +1026,6 @@ namespace // static fontp cache after a few cycles (see vertexbuffer fixture). // =================================================================== -namespace -{ inline void ensureRenderLLFontGL() { static bool initialized = false; @@ -1005,22 +1044,103 @@ namespace ~llfontgl_render_data() = default; }; - typedef test_group llfontgl_render_test; - typedef llfontgl_render_test::object llfontgl_render_object; - tut::llfontgl_render_test llfontgl_render_testcase("LLFontGLRender"); -} + + // What a reflow costs the shaper. LLTextBase wraps by asking + // maxDrawableBytes how much of the remaining text fits, then asking + // getWidthF32Bytes how wide that answer is -- two questions about the same + // run, per line, per segment, and every one of them can be a HarfBuzz + // pass. The shape cache turns the repeats into lookups only when the two + // calls agree on the slice they are asking about. + // + // Counted in cache mutations, which rise once per insert, so the number is + // exactly "how many runs had to be shaped". Timing would not survive a + // build machine; this does. + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: a reflow costs no shapes at the same width and few in a drag", "[LLFontGL]") + { + if (!fileExists(kFontsXml)) + SKIP("fonts.xml not found"); + LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, + LLSD(), /*create_gl_textures=*/true); + LLFontGL* font = LLFontGL::getFontSansSerif(); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); + + // A chat line long enough to wrap several times at a readable width. + const std::string paragraph = + "The quick brown fox jumps over the lazy dog while the " + "second sentence carries on for long enough to wrap a few " + "times at any sensible chat width at all."; + + // One pass of LLTextBase's wrap loop over one segment, counting the + // shapes it costs. Mirrors LLNormalTextSegment::getNumBytes followed + // by getDimensionsF32. + auto wrap_once = [&](F32 width) -> size_t + { + const size_t before = ALFontShaping::cacheMutationCount(); + S32 at = 0; + while (at < (S32)paragraph.size()) + { + const std::string_view rest = std::string_view(paragraph).substr((size_t)at); + const S32 fits = font->maxDrawableBytes(rest, width, + (S32)rest.size(), + LLFontGL::WORD_BOUNDARY_IF_POSSIBLE); + if (fits <= 0) + { + break; + } + // The width of exactly what fit, which is what the wrap loop + // asks for next to place the line. + (void)font->getWidthF32Bytes(paragraph, at, fits, true); + at += fits; + } + return ALFontShaping::cacheMutationCount() - before; + }; + + // Cold, so every distinct slice counts. + ALFontShaping::clearCache(); + const size_t first = wrap_once(220.f); + UNSCOPED_INFO("wrapping shapes something"); + CHECK(first > 0); + + // The same width again: every slice is already cached, so a repeated + // reflow at an unchanged width must cost nothing. + const size_t again = wrap_once(220.f); + UNSCOPED_INFO("re-wrapping at the same width re-shapes nothing"); + CHECK(again == size_t(0)); + + // A drag-resize. Each frame moves the width a pixel or two, and the + // wrap points move with it, so the slices differ from the ones the + // last frame cached. This is what a resize costs per frame per + // segment, and it is the number worth watching. + size_t dragging = 0; + for (S32 px = 219; px >= 200; --px) + { + dragging += wrap_once((F32)px); + } + + // Not an assertion about a good number -- an assertion that the number + // is bounded by the work there is to do. Twenty widths over a + // paragraph that wraps into a handful of lines: if this ever exceeds + // two shapes per line per width, the two callers have stopped agreeing + // on their slices again. + const size_t lines_per_wrap = first; + std::printf("[reflow shaping] cold=%zu same-width=%zu drag-20px=%zu\n", + first, again, dragging); + UNSCOPED_INFO("the drag stays within two shapes per line per width"); + CHECK(dragging <= 2 * lines_per_wrap * 20); + } // First end-to-end render() call with LEFT/BASELINE places glyph // pixels into the framebuffer at the requested x. Pins the full // chain: shape → atlas → vertex buffer → drawArrays → fragment // output → glReadPixels. - template<> template<> - void llfontgl_render_object::test<1>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: renderBytes of one character emits one textured quad", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); gl.clearFramebuffer(); const std::string s = "A"; @@ -1042,16 +1162,21 @@ namespace // Force any pending verts to draw. gGL.flush(); gGL.endList(); - ensure_equals("render returned 1 char", n, 1); + UNSCOPED_INFO("render returned 1 char"); + CHECK(n == 1); U32 verts = 0; for (const LLVertexBufferData& entry : capture) { verts += entry.mCount; } - ensure_equals("one glyph is one quad", verts, (U32)6); - ensure("the quad carries an atlas texture", - !capture.empty() && capture.front().mTexName != 0); + UNSCOPED_INFO("one glyph is one quad"); + CHECK(verts == (U32)6); + { + INFO("the quad carries an atlas texture"); + REQUIRE_FALSE(capture.empty()); + CHECK(capture.front().mTexName != 0); + } } // HCENTER and RIGHT alignment branches: render() must complete @@ -1060,11 +1185,10 @@ namespace // verification is brittle in this test setup (stub shader / state // accumulation); this pins crash-free traversal of all three // halign branches at llfontgl.cpp:354-372. - template<> template<> - void llfontgl_render_object::test<2>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: renderBytes draws every byte whatever the horizontal alignment", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); const std::string s = "ABC"; @@ -1078,20 +1202,22 @@ namespace LLFontGL::RIGHT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, 3); gGL.flush(); - ensure_equals("LEFT render returns 3 bytes", n_left, 3); - ensure_equals("HCENTER render returns 3 bytes", n_cen, 3); - ensure_equals("RIGHT render returns 3 bytes", n_right, 3); + UNSCOPED_INFO("LEFT render returns 3 bytes"); + CHECK(n_left == 3); + UNSCOPED_INFO("HCENTER render returns 3 bytes"); + CHECK(n_cen == 3); + UNSCOPED_INFO("RIGHT render returns 3 bytes"); + CHECK(n_right == 3); } // VAlign branches: render() must complete through TOP, VCENTER, // BASELINE, and BOTTOM without crashing; return value matches // requested char count for each. Pins the valign switch at // llfonttextcache.cpp:123-138. - template<> template<> - void llfontgl_render_object::test<3>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: renderBytes draws every byte whatever the vertical alignment", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); const std::string s = "Hi"; @@ -1105,19 +1231,21 @@ namespace LLFontGL::LEFT, LLFontGL::BOTTOM, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, 2); gGL.flush(); - ensure_equals("TOP render returns 2 bytes", n_top, 2); - ensure_equals("BASELINE render returns 2 bytes", n_bs, 2); - ensure_equals("BOTTOM render returns 2 bytes", n_btm, 2); + UNSCOPED_INFO("TOP render returns 2 bytes"); + CHECK(n_top == 2); + UNSCOPED_INFO("BASELINE render returns 2 bytes"); + CHECK(n_bs == 2); + UNSCOPED_INFO("BOTTOM render returns 2 bytes"); + CHECK(n_btm == 2); } // Style flip across renders: render with NORMAL style, then BOLD. // Both must succeed. Pins style propagation through render() — // 00f3a4e93d's regression invalidation gate. - template<> template<> - void llfontgl_render_object::test<4>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: renderBytes draws every byte whatever the style", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); const std::string s = "Hello"; @@ -1131,9 +1259,12 @@ namespace LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::UNDERLINE, LLFontGL::NO_SHADOW, 5); gGL.flush(); - ensure_equals("NORMAL render returns 5 bytes", n_norm, 5); - ensure_equals("BOLD render returns 5 bytes", n_bold, 5); - ensure_equals("UNDERLINE render returns 5 bytes", n_und, 5); + UNSCOPED_INFO("NORMAL render returns 5 bytes"); + CHECK(n_norm == 5); + UNSCOPED_INFO("BOLD render returns 5 bytes"); + CHECK(n_bold == 5); + UNSCOPED_INFO("UNDERLINE render returns 5 bytes"); + CHECK(n_und == 5); } // Shadow rendering paths: render with NO_SHADOW, DROP_SHADOW, and @@ -1143,11 +1274,10 @@ namespace // to be gUIProgram, which the fixture ensures. Pins shadow // rendering doesn't crash; visible-shadow geometry verification is // out of scope for this fixture (no production shadow shader). - template<> template<> - void llfontgl_render_object::test<5>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: renderBytes draws every byte whatever the shadow", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); const std::string s = "Hi"; @@ -1161,9 +1291,12 @@ namespace LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::DROP_SHADOW_SOFT, 2); gGL.flush(); - ensure_equals("NO_SHADOW returns 2", n_no, 2); - ensure_equals("DROP_SHADOW returns 2", n_drop, 2); - ensure_equals("DROP_SHADOW_SOFT returns 2", n_soft, 2); + UNSCOPED_INFO("NO_SHADOW returns 2"); + CHECK(n_no == 2); + UNSCOPED_INFO("DROP_SHADOW returns 2"); + CHECK(n_drop == 2); + UNSCOPED_INFO("DROP_SHADOW_SOFT returns 2"); + CHECK(n_soft == 2); } // Mid-render glyph rasterization must not misdirect the pending @@ -1181,15 +1314,16 @@ namespace // directly observable: render a Latin prefix plus a never-yet- // rasterized emoji and require the prefix batch to carry the // grayscale atlas texture its UVs were built against. - template<> template<> - void llfontgl_render_object::test<6>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: mid-render glyph rasterization keeps the pending batch's texture", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); const LLFontFreetype* head = font->getFontFreetype(); - ensure("head freetype resolves", head != nullptr); + UNSCOPED_INFO("head freetype resolves"); + REQUIRE(head != nullptr); // U+1F995 SAUROPOD — obscure enough that no other test in this // binary rasterizes it. The mid-render upload only fires on a @@ -1198,11 +1332,11 @@ namespace U32 emoji_idx = 0; const LLFontFreetype* emoji_face = head->selectShapingFace(emoji_cp, emoji_idx); if (!emoji_face || emoji_face == head || emoji_idx == 0) - skip("no emoji fallback covers U+1F995 in this harness"); - ensure("emoji fallback has a face wrapper", - emoji_face->getFontFace() != nullptr); + SKIP("no emoji fallback covers U+1F995 in this harness"); + UNSCOPED_INFO("emoji fallback has a face wrapper"); + REQUIRE(emoji_face->getFontFace() != nullptr); if (emoji_face->getFontFace()->findGlyphInfo(emoji_idx, EFontGlyphType::Color)) - skip("U+1F995 already rasterized; mid-render upload won't fire"); + SKIP("U+1F995 already rasterized; mid-render upload won't fire"); const std::string s = "stomp \xF0\x9F\xA6\x95 check"; @@ -1213,42 +1347,55 @@ namespace LLFontGL::NORMAL, LLFontGL::NO_SHADOW, (S32)s.size()); gGL.endList(); - ensure_equals("render consumed the whole string", n, (S32)s.size()); + UNSCOPED_INFO("render consumed the whole string"); + REQUIRE(n == (S32)s.size()); // Expected textures, resolved through the glyph entries the render // itself produced. use_color defaulted true, so the render looked // glyphs up as Color; querying the same way returns the same // entries (and therefore the same atlas pages) it drew from. const LLFontGlyphInfo* latin_gi = head->getGlyphInfo((llwchar)'s', EFontGlyphType::Color); - ensure("latin glyph cached", latin_gi && latin_gi->mSourceFace); + { + INFO("latin glyph cached"); + REQUIRE(latin_gi); + REQUIRE(latin_gi->mSourceFace); + } const auto& latin_entry = latin_gi->mPhaseSlots[0].mBitmapEntry; LLImageGL* latin_img = latin_gi->mSourceFace->getBitmapCache()->getImageGL( latin_entry.first, (U32)latin_entry.second); - ensure("latin atlas page live", latin_img != nullptr); + UNSCOPED_INFO("latin atlas page live"); + REQUIRE(latin_img != nullptr); const U32 latin_tex = latin_img->getTexName(); const LLFontGlyphInfo* emoji_gi = head->getGlyphInfo(emoji_cp, EFontGlyphType::Color); - ensure("emoji glyph cached", emoji_gi && emoji_gi->mSourceFace); + { + INFO("emoji glyph cached"); + REQUIRE(emoji_gi); + REQUIRE(emoji_gi->mSourceFace); + } const auto& emoji_entry = emoji_gi->mPhaseSlots[0].mBitmapEntry; LLImageGL* emoji_img = emoji_gi->mSourceFace->getBitmapCache()->getImageGL( emoji_entry.first, (U32)emoji_entry.second); - ensure("emoji atlas page live", emoji_img != nullptr); + UNSCOPED_INFO("emoji atlas page live"); + REQUIRE(emoji_img != nullptr); const U32 emoji_tex = emoji_img->getTexName(); - ensure("string spans two distinct atlas textures", latin_tex != emoji_tex); - ensure("capture produced batches", !capture.empty()); + UNSCOPED_INFO("string spans two distinct atlas textures"); + CHECK(latin_tex != emoji_tex); + UNSCOPED_INFO("capture produced batches"); + REQUIRE_FALSE(capture.empty()); // The first captured batch is the Latin prefix, flushed when the // glyph stream switches to the emoji's atlas — exactly the batch // the mid-render upload used to misdirect. - ensure_equals("prefix batch carries its own atlas texture", - capture.front().mTexName, latin_tex); + UNSCOPED_INFO("prefix batch carries its own atlas texture"); + CHECK(capture.front().mTexName == latin_tex); // And every batch in the capture must reference one of the two // atlas pages this string actually draws from. for (const LLVertexBufferData& entry : capture) { - ensure("batch texture is one of the string's atlas pages", - entry.mTexName == latin_tex || entry.mTexName == emoji_tex); + UNSCOPED_INFO("batch texture is one of the string's atlas pages"); + REQUIRE((entry.mTexName == latin_tex || entry.mTexName == emoji_tex)); } } @@ -1256,26 +1403,28 @@ namespace // a codepoint count, a glyph count or a cluster count, and every existing // assertion about it is over ASCII. Three characters of three bytes each // tell the four apart: nine, and nothing else. - template<> template<> - void llfontgl_render_object::test<7>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: renderBytes counts bytes rather than characters", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); const std::string s = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E"; const S32 total = (S32)s.size(); - ensure_equals("the fixture string is nine bytes", total, (S32)9); + UNSCOPED_INFO("the fixture string is nine bytes"); + CHECK(total == (S32)9); if (font->getWidthF32Bytes(s, 0, total, false) <= 0.f) - skip("font produced no width for the sample"); + SKIP("font produced no width for the sample"); const S32 drawn = font->renderBytes(s, 0, 100.f, 100.f, LLColor4::white, LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, total); gGL.flush(); - ensure_equals("renderBytes returns bytes, not characters", drawn, total); + UNSCOPED_INFO("renderBytes returns bytes, not characters"); + CHECK(drawn == total); // A budget of one character is three bytes, and drawing from an offset // reports what it drew rather than where it stopped. @@ -1284,15 +1433,16 @@ namespace LLFontGL::NORMAL, LLFontGL::NO_SHADOW, 3); gGL.flush(); - ensure_equals("a one-character budget is three bytes", one, (S32)3); + UNSCOPED_INFO("a one-character budget is three bytes"); + CHECK(one == (S32)3); const S32 tail = font->renderBytes(s, 3, 100.f, 140.f, LLColor4::white, LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, S32_MAX); gGL.flush(); - ensure_equals("drawing from an offset returns the bytes it drew", - tail, total - 3); + UNSCOPED_INFO("drawing from an offset returns the bytes it drew"); + CHECK(tail == total - 3); } // Clipping is per cluster, and the count says so. Several glyphs can carry @@ -1301,15 +1451,16 @@ namespace // a per-glyph clip can paint a base and drop the mark that belongs to it. // That is not a clipped syllable, it is a different syllable, and the // count reported back calls the whole cluster undrawn either way. - template<> template<> - void llfontgl_render_object::test<8>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: renderBytes clips whole clusters and counts what it drew", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); const LLFontFreetype* ft = font->getFontFreetype(); - ensure("freetype present", ft != nullptr); + UNSCOPED_INFO("freetype present"); + REQUIRE(ft != nullptr); // Devanagari, which shapes two glyphs to a cluster with an advance on // each -- the shape that a per-glyph clip splits. @@ -1318,13 +1469,13 @@ namespace const S32 total = (S32)s.size(); const F32 full = font->getWidthF32Bytes(s, 0, total, false); if (full <= 0.f) - skip("no Devanagari coverage in the test font set"); + SKIP("no Devanagari coverage in the test font set"); // Cluster boundaries from the shaper itself rather than by hand: which // bytes are boundaries is the font's business, not this test's. const auto& glyphs = ALFontShaping::shapeLine(ft, s, 0, (size_t)total); if (glyphs.empty()) - skip("Devanagari did not shape"); + SKIP("Devanagari did not shape"); std::set boundaries; for (const ALShapedGlyph& g : glyphs) { @@ -1353,8 +1504,8 @@ namespace verts += entry.mCount; } - ensure("a clipped draw stops on a cluster boundary", - boundaries.count(n) == 1); + UNSCOPED_INFO("a clipped draw stops on a cluster boundary"); + REQUIRE(boundaries.count(n) == 1); // Exactly the glyphs of the clusters the count claims. A per-glyph // clip emits the glyphs of the overflowing cluster that came before @@ -1374,11 +1525,14 @@ namespace // its own start_x while getWidthF32Bytes divides the scale back // out. Testing the drawn extent wants the vertex positions out of // the capture, not a re-measurement. - ensure_equals("quads drawn match the bytes reported", verts, expected); + UNSCOPED_INFO("quads drawn match the bytes reported"); + REQUIRE(verts == expected); if (n > 0 && n < total) ++checked; } - ensure("the sweep reached a partially-clipped draw", checked > 0); + UNSCOPED_INFO("the sweep reached a partially-clipped draw"); + CHECK(checked > 0); } + // renderBytes places a right-aligned or centred string by measuring it, and // it takes that measurement from the glyphs it is about to draw rather than // shaping the text a second time. The two have to agree exactly: the number @@ -1389,13 +1543,13 @@ namespace // right_x is the observable end of the drawn run, so LEFT-aligned it is the // origin plus the width. That is what pins the shared sum against the // independent walk. - template<> template<> - void llfontgl_render_object::test<9>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: renderBytes ends a run exactly where getWidthF32Bytes measures it", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); const std::string samples[] = { std::string("Hello world"), @@ -1424,92 +1578,9 @@ namespace gGL.flush(); // right_x is in the same unscaled space the origin was given in. - ensure_equals("the drawn run ends where the measurement says", - right_x, 40.f + expect); - } - } - - // What a reflow costs the shaper. LLTextBase wraps by asking - // maxDrawableBytes how much of the remaining text fits, then asking - // getWidthF32Bytes how wide that answer is -- two questions about the same - // run, per line, per segment, and every one of them can be a HarfBuzz - // pass. The shape cache turns the repeats into lookups only when the two - // calls agree on the slice they are asking about. - // - // Counted in cache mutations, which rise once per insert, so the number is - // exactly "how many runs had to be shaped". Timing would not survive a - // build machine; this does. - template<> template<> - void llfontgl_object::test<26>() - { - if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); - LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, - LLSD(), /*create_gl_textures=*/true); - LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); - - // A chat line long enough to wrap several times at a readable width. - const std::string paragraph = - "The quick brown fox jumps over the lazy dog while the " - "second sentence carries on for long enough to wrap a few " - "times at any sensible chat width at all."; - - // One pass of LLTextBase's wrap loop over one segment, counting the - // shapes it costs. Mirrors LLNormalTextSegment::getNumBytes followed - // by getDimensionsF32. - auto wrap_once = [&](F32 width) -> size_t - { - const size_t before = ALFontShaping::cacheMutationCount(); - S32 at = 0; - while (at < (S32)paragraph.size()) - { - const std::string_view rest = std::string_view(paragraph).substr((size_t)at); - const S32 fits = font->maxDrawableBytes(rest, width, - (S32)rest.size(), - LLFontGL::WORD_BOUNDARY_IF_POSSIBLE); - if (fits <= 0) - { - break; - } - // The width of exactly what fit, which is what the wrap loop - // asks for next to place the line. - (void)font->getWidthF32Bytes(paragraph, at, fits, true); - at += fits; - } - return ALFontShaping::cacheMutationCount() - before; - }; - - // Cold, so every distinct slice counts. - ALFontShaping::clearCache(); - const size_t first = wrap_once(220.f); - ensure("wrapping shapes something", first > 0); - - // The same width again: every slice is already cached, so a repeated - // reflow at an unchanged width must cost nothing. - const size_t again = wrap_once(220.f); - ensure_equals("re-wrapping at the same width re-shapes nothing", again, size_t(0)); - - // A drag-resize. Each frame moves the width a pixel or two, and the - // wrap points move with it, so the slices differ from the ones the - // last frame cached. This is what a resize costs per frame per - // segment, and it is the number worth watching. - size_t dragging = 0; - for (S32 px = 219; px >= 200; --px) - { - dragging += wrap_once((F32)px); + UNSCOPED_INFO("the drawn run ends where the measurement says"); + REQUIRE(right_x == 40.f + expect); } - - // Not an assertion about a good number -- an assertion that the number - // is bounded by the work there is to do. Twenty widths over a - // paragraph that wraps into a handful of lines: if this ever exceeds - // two shapes per line per width, the two callers have stopped agreeing - // on their slices again. - const size_t lines_per_wrap = first; - std::printf("[reflow shaping] cold=%zu same-width=%zu drag-20px=%zu\n", - first, again, dragging); - ensure("the drag stays within two shapes per line per width", - dragging <= 2 * lines_per_wrap * 20); } // An ellipsized draw buys itself padding by measuring "....", and that @@ -1520,13 +1591,13 @@ namespace // to be read after the dots are measured. Read before, the first // ellipsized string of a session trips a tripwire for a walk that has not // started yet, which on the login screen is every session. - template<> template<> - void llfontgl_render_object::test<10>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: an ellipsized draw on a cold cache shapes the dots mid-draw", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); const std::string s = "a string rather wider than the budget it is given"; @@ -1534,7 +1605,8 @@ namespace // draw below is the ellipsis measurement. ALFontShaping::clearCache(); const F32 full = font->getWidthF32(s); - ensure("the sample string has width", full > 0.f); + UNSCOPED_INFO("the sample string has width"); + CHECK(full > 0.f); const size_t before = ALFontShaping::cacheMutationCount(); @@ -1549,22 +1621,21 @@ namespace gGL.flush(); gGL.endList(); - ensure("an ellipsized draw stops short of the whole string", - n < (S32)s.size()); + UNSCOPED_INFO("an ellipsized draw stops short of the whole string"); + REQUIRE(n < (S32)s.size()); // Without this the test proves nothing: it is the dots landing in the // cache mid-draw that puts the snapshot and the count out of step. - ensure("the ellipsis measurement shaped inside the draw", - ALFontShaping::cacheMutationCount() > before); + UNSCOPED_INFO("the ellipsis measurement shaped inside the draw"); + CHECK(ALFontShaping::cacheMutationCount() > before); } // A string placed by placeGlyphs and drawn by renderGlyphs lands on the // pixels renderUTF8 puts it on, whichever way it is aligned; and runs // drawn in one call by renderGlyphRuns are the runs drawn apiece. - template<> template<> - void llfontgl_render_object::test<11>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: renderGlyphs matches renderUTF8 and renderGlyphRuns matches renderGlyphs", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); const S32 w = ll_test::HeadlessGL::WIDTH; const S32 h = ll_test::HeadlessGL::HEIGHT; const auto frame = [&](const std::function& draw) { @@ -1588,7 +1659,8 @@ namespace const LLFontGL::HAlign aligns[] = { LLFontGL::LEFT, LLFontGL::RIGHT, LLFontGL::HCENTER }; for (LLFontGL* font : { LLFontGL::getFontMonospace(), LLFontGL::getFontSansSerif() }) { - ensure("font resolves", font != nullptr); + UNSCOPED_INFO("font resolves"); + REQUIRE(font != nullptr); for (const std::string s : { "7", "42", "12345", "Hello, world" }) { for (LLFontGL::HAlign halign : aligns) @@ -1601,8 +1673,10 @@ namespace const F32 shift = font->placeGlyphs(s, halign, placed); const std::vector colors(placed.size(), LLColor4U(255, 255, 255, 255)); const std::vector drawn = frame([&] { font->renderGlyphs(placed.data(), colors.data(), placed.size(), 128.f + shift, 100.f); }); - ensure(what + ": drawn at all", lit(said)); - ensure(what + ": the same pixels", said == drawn); + UNSCOPED_INFO(what + ": drawn at all"); + REQUIRE(lit(said)); + UNSCOPED_INFO(what + ": the same pixels"); + REQUIRE(said == drawn); } } } @@ -1620,7 +1694,9 @@ namespace const LLFontGL::GlyphRun runs[] = { { first.data(), red.data(), first.size(), 20.f, 50.f }, { second.data(), green.data(), second.size(), 100.f, 150.f } }; const std::vector together = frame([&] { font->renderGlyphRuns(runs, 2); }); - ensure("runs drawn at all", lit(apiece)); - ensure("runs in one call are the runs apiece", apiece == together); + UNSCOPED_INFO("runs drawn at all"); + CHECK(lit(apiece)); + UNSCOPED_INFO("runs in one call are the runs apiece"); + CHECK(apiece == together); } } diff --git a/indra/llrender/tests/llfontregistry_gl_test.cpp b/indra/llrender/tests/llfontregistry_gl_test.cpp index 700ace4bbb..146911ba81 100644 --- a/indra/llrender/tests/llfontregistry_gl_test.cpp +++ b/indra/llrender/tests/llfontregistry_gl_test.cpp @@ -37,7 +37,7 @@ #include "llfonttest_helpers.h" #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include @@ -151,65 +151,64 @@ namespace tut } }; - typedef test_group llfontregistry_gl_test; - typedef llfontregistry_gl_test::object llfontregistry_gl_object; -namespace -{ - tut::llfontregistry_gl_test llfontregistry_gl_testcase("LLFontRegistry-GL"); -} - // createFont with mCreateGLTextures=true must produce a head whose // bitmap cache, after rasterizing the ASCII range, has at least one // grayscale page bound to a live GL texture. - template<> template<> - void llfontregistry_gl_object::test<1>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: createFont with GL textures backs the ASCII atlas with a live texture", "[LLFontRegistry-GL]") { if (!interFontPresent()) { - skip("InterVariable.woff2 not present in test data dir"); + SKIP("InterVariable.woff2 not present in test data dir"); return; } - ensure("parse ok", loadXml(kInterXml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(kInterXml)); LLFontGL* font = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - ensure("getFont resolves Inter/Small", font != nullptr); + UNSCOPED_INFO("getFont resolves Inter/Small"); + REQUIRE(font != nullptr); font->generateASCIIglyphs(); const LLFontFreetype* ft = font->getFontFreetype(); - ensure("LLFontFreetype present", ft != nullptr); + UNSCOPED_INFO("LLFontFreetype present"); + REQUIRE(ft != nullptr); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - ensure("bitmap cache present", cache != nullptr); - ensure("at least one grayscale atlas page", - cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); + UNSCOPED_INFO("bitmap cache present"); + REQUIRE(cache != nullptr); + UNSCOPED_INFO("at least one grayscale atlas page"); + CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); LLImageGL* page = cache->getImageGL(EFontGlyphType::Grayscale, 0); - ensure("page 0 LLImageGL present", page != nullptr); - ensure("page 0 has a live GL texture name", - page->getTexName() != 0); + UNSCOPED_INFO("page 0 LLImageGL present"); + REQUIRE(page != nullptr); + UNSCOPED_INFO("page 0 has a live GL texture name"); + CHECK(page->getTexName() != 0); } // getFont(desc) must return the same LLFontGL pointer on a repeat // call — the registry caches by descriptor and a second hit must // not re-create or re-rasterize the head. - template<> template<> - void llfontregistry_gl_object::test<2>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: getFont returns the cached LLFontGL on a repeat call", "[LLFontRegistry-GL]") { if (!interFontPresent()) { - skip("InterVariable.woff2 not present in test data dir"); + SKIP("InterVariable.woff2 not present in test data dir"); return; } - ensure("parse ok", loadXml(kInterXml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(kInterXml)); LLFontGL* first = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - ensure("first getFont resolves", first != nullptr); + UNSCOPED_INFO("first getFont resolves"); + REQUIRE(first != nullptr); LLFontGL* second = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - ensure("second getFont resolves", second != nullptr); - ensure("repeat getFont returns the same LLFontGL pointer", - first == second); + UNSCOPED_INFO("second getFont resolves"); + REQUIRE(second != nullptr); + UNSCOPED_INFO("repeat getFont returns the same LLFontGL pointer"); + CHECK(first == second); } // destroyGL must drop GL textures and reset the face's bitmap cache @@ -219,33 +218,34 @@ namespace // the LLImageGL vector entirely, so post-destroyGL the page count // is 0 (not "page count preserved with texname=0"). The next render // through the face re-allocates fresh atlas pages. - template<> template<> - void llfontregistry_gl_object::test<3>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: destroyGL empties the atlas but keeps the registry's LLFontGL", "[LLFontRegistry-GL]") { if (!interFontPresent()) { - skip("InterVariable.woff2 not present in test data dir"); + SKIP("InterVariable.woff2 not present in test data dir"); return; } - ensure("parse ok", loadXml(kInterXml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(kInterXml)); LLFontGL* font = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - ensure("getFont resolves", font != nullptr); + UNSCOPED_INFO("getFont resolves"); + REQUIRE(font != nullptr); font->generateASCIIglyphs(); const LLFontBitmapCache* cache = font->getFontFreetype()->getFontBitmapCache(); - ensure("at least one atlas page before destroyGL", - cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); - ensure("texname live before destroyGL", - cache->getImageGL(EFontGlyphType::Grayscale, 0)->getTexName() != 0); + UNSCOPED_INFO("at least one atlas page before destroyGL"); + CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); + UNSCOPED_INFO("texname live before destroyGL"); + CHECK(cache->getImageGL(EFontGlyphType::Grayscale, 0)->getTexName() != 0); reg.destroyGL(); - ensure_equals("page vector cleared after destroyGL", - (S32)cache->getNumBitmaps(EFontGlyphType::Grayscale), 0); - ensure("registry entry survived destroyGL — same LLFontGL pointer", - reg.getFont(LLFontDescriptor("Inter", "Small", 0)) == font); + UNSCOPED_INFO("page vector cleared after destroyGL"); + CHECK((S32)cache->getNumBitmaps(EFontGlyphType::Grayscale) == 0); + UNSCOPED_INFO("registry entry survived destroyGL — same LLFontGL pointer"); + CHECK(reg.getFont(LLFontDescriptor("Inter", "Small", 0)) == font); } // End-to-end runtime size-change reload. Verify that wiping registry @@ -254,12 +254,11 @@ namespace // size. Indirect because LLFontFreetype::mPointSize is private — // ascender height is FreeType metrics * pointSize/units_per_EM, so // doubling the point size doubles the ascender (within rounding). - template<> template<> - void llfontregistry_gl_object::test<4>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: a reload at twice the point size roughly doubles the ascender", "[LLFontRegistry-GL]") { if (!interFontPresent()) { - skip("InterVariable.woff2 not present in test data dir"); + SKIP("InterVariable.woff2 not present in test data dir"); return; } @@ -271,11 +270,14 @@ namespace " " " " ""; - ensure("v1 parse ok", loadXml(kV1)); + UNSCOPED_INFO("v1 parse ok"); + CHECK(loadXml(kV1)); LLFontGL* font_v1 = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - ensure("v1 getFont resolves", font_v1 != nullptr); + UNSCOPED_INFO("v1 getFont resolves"); + REQUIRE(font_v1 != nullptr); F32 ascender_v1 = font_v1->getAscenderHeight(); - ensure("v1 ascender > 0", ascender_v1 > 0.f); + UNSCOPED_INFO("v1 ascender > 0"); + CHECK(ascender_v1 > 0.f); // Wipe + re-parse with 2x size. simulateReload(); @@ -286,16 +288,19 @@ namespace " " " " ""; - ensure("v2 parse ok", loadXml(kV2)); + UNSCOPED_INFO("v2 parse ok"); + CHECK(loadXml(kV2)); LLFontGL* font_v2 = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - ensure("v2 getFont resolves", font_v2 != nullptr); + UNSCOPED_INFO("v2 getFont resolves"); + REQUIRE(font_v2 != nullptr); F32 ascender_v2 = font_v2->getAscenderHeight(); - ensure("v2 ascender > 0", ascender_v2 > 0.f); + UNSCOPED_INFO("v2 ascender > 0"); + CHECK(ascender_v2 > 0.f); // Doubling pt should ~double ascender (allow generous tolerance for // FreeType rounding: 1.7x lower bound is comfortably above 1.0x). - ensure("ascender scales with reloaded point size", - ascender_v2 > ascender_v1 * 1.7f); + UNSCOPED_INFO("ascender scales with reloaded point size"); + CHECK(ascender_v2 > ascender_v1 * 1.7f); } // End-to-end override -> clear -> override-again with DIFFERENT font @@ -306,14 +311,13 @@ namespace // verify the head's freetype actually changed AND the point size moved. // Pins both halves of the loop: file swap on override change, AND // size routing through the head-face chain. - template<> template<> - void llfontregistry_gl_object::test<5>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: clearing an override reloads the head with another file and size", "[LLFontRegistry-GL]") { const std::string od_path = std::string(kFontDir) + "OpenDyslexic-Regular.otf"; const std::string inter_path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(od_path) || !fileExists(inter_path)) { - skip("OpenDyslexic-Regular.otf or InterVariable.woff2 not present in test data dir"); + SKIP("OpenDyslexic-Regular.otf or InterVariable.woff2 not present in test data dir"); return; } @@ -333,25 +337,31 @@ namespace ""; // First load: override SansSerifBase -> OpenDyslexic. - ensure("parse v1 ok", loadXml(kXml)); + UNSCOPED_INFO("parse v1 ok"); + CHECK(loadXml(kXml)); LLSD overrides; overrides["SansSerifBase"] = "OpenDyslexic"; resolve(overrides); LLFontGL* font_with = reg.getFont(LLFontDescriptor("SansSerif", "Large", 0)); - ensure("getFont with override resolves", font_with != nullptr); + UNSCOPED_INFO("getFont with override resolves"); + REQUIRE(font_with != nullptr); F32 ascender_with = font_with->getAscenderHeight(); - ensure("ascender with override > 0", ascender_with > 0.f); + UNSCOPED_INFO("ascender with override > 0"); + CHECK(ascender_with > 0.f); // Wipe + re-parse with no override. simulateReload(); - ensure("parse v2 ok", loadXml(kXml)); + UNSCOPED_INFO("parse v2 ok"); + CHECK(loadXml(kXml)); resolve(LLSD()); LLFontGL* font_no = reg.getFont(LLFontDescriptor("SansSerif", "Large", 0)); - ensure("getFont without override resolves", font_no != nullptr); + UNSCOPED_INFO("getFont without override resolves"); + REQUIRE(font_no != nullptr); F32 ascender_no = font_no->getAscenderHeight(); - ensure("ascender without override > 0", ascender_no > 0.f); + UNSCOPED_INFO("ascender without override > 0"); + CHECK(ascender_no > 0.f); // The two states must produce visibly different ascender heights: // With override: OpenDyslexic at 14pt — large intrinsic + larger pt. @@ -362,8 +372,8 @@ namespace const F32 diff = (ascender_with > ascender_no) ? (ascender_with - ascender_no) : (ascender_no - ascender_with); - ensure("override and no-override produce distinguishable ascenders", - diff > ascender_no * 0.1f); + UNSCOPED_INFO("override and no-override produce distinguishable ascenders"); + CHECK(diff > ascender_no * 0.1f); } // Per-family on a fallback family pins THAT family's files at @@ -373,14 +383,13 @@ namespace // and find the DejaVu fallback — its getAscenderHeight() reflects // 8pt rendering, not 16pt. Every is an absolute pin (no // force keyword needed). - template<> template<> - void llfontregistry_gl_object::test<6>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: a fallback family's pins its files below the head's size", "[LLFontRegistry-GL]") { const std::string inter_path = std::string(kFontDir) + "InterVariable.woff2"; const std::string dejavu_path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(inter_path) || !fileExists(dejavu_path)) { - skip("InterVariable.woff2 or DejaVuSans.woff2 not present in test data dir"); + SKIP("InterVariable.woff2 or DejaVuSans.woff2 not present in test data dir"); return; } @@ -397,14 +406,17 @@ namespace " " ""; - ensure("parse ok", loadXml(kXml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(kXml)); resolve(); LLFontGL* font = reg.getFont(LLFontDescriptor("Inter", "Large", 0)); - ensure("getFont resolves", font != nullptr); + UNSCOPED_INFO("getFont resolves"); + REQUIRE(font != nullptr); // Head is Inter at chain's 16pt. F32 head_ascender = font->getAscenderHeight(); - ensure("head ascender > 0", head_ascender > 0.f); + UNSCOPED_INFO("head ascender > 0"); + CHECK(head_ascender > 0.f); // Find the DejaVu fallback in the chain. Its mPointSize should be // the per-family pin 8.0, NOT the chain's 16.0. @@ -418,16 +430,18 @@ namespace break; } } - ensure("DejaVu fallback present in chain", dejavu_fallback != nullptr); + UNSCOPED_INFO("DejaVu fallback present in chain"); + REQUIRE(dejavu_fallback != nullptr); F32 fb_ascender = dejavu_fallback->getAscenderHeight(); - ensure("DejaVu fallback ascender > 0", fb_ascender > 0.f); + UNSCOPED_INFO("DejaVu fallback ascender > 0"); + CHECK(fb_ascender > 0.f); // The pinned fallback at 8pt must be substantially smaller than // the head's 16pt rendering. Demand >40% smaller — comfortably // separates "pin applied" from "fell through to chain pt". - ensure("pinned fallback ascender is substantially smaller than head", - fb_ascender < head_ascender * 0.6f); + UNSCOPED_INFO("pinned fallback ascender is substantially smaller than head"); + CHECK(fb_ascender < head_ascender * 0.6f); } // End-to-end override SWAP between two distinct pinned sources, with @@ -438,14 +452,13 @@ namespace // back-to-back override swap, with no leftover state from the first // source. Mirrors the picker workflow of moving between two custom // UI fonts that each declare their own preferred size. - template<> template<> - void llfontregistry_gl_object::test<7>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: an override swap between pinned sources reloads at the new pin", "[LLFontRegistry-GL]") { const std::string od_path = std::string(kFontDir) + "OpenDyslexic-Regular.otf"; const std::string inter_path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(od_path) || !fileExists(inter_path)) { - skip("OpenDyslexic-Regular.otf or InterVariable.woff2 not present in test data dir"); + SKIP("OpenDyslexic-Regular.otf or InterVariable.woff2 not present in test data dir"); return; } @@ -466,32 +479,38 @@ namespace ""; // Override A: Target -> OpenDyslexic. Per-file pin lands head at 14pt. - ensure("v1 parse ok", loadXml(kXml)); + UNSCOPED_INFO("v1 parse ok"); + CHECK(loadXml(kXml)); LLSD ovr_a; ovr_a["Target"] = "OpenDyslexic"; resolve(ovr_a); LLFontGL* font_a = reg.getFont(LLFontDescriptor("Target", "Large", 0)); - ensure("getFont under OpenDyslexic override", font_a != nullptr); + UNSCOPED_INFO("getFont under OpenDyslexic override"); + REQUIRE(font_a != nullptr); F32 asc_a = font_a->getAscenderHeight(); - ensure("OpenDyslexic ascender > 0", asc_a > 0.f); + UNSCOPED_INFO("OpenDyslexic ascender > 0"); + CHECK(asc_a > 0.f); // Swap: Target -> Inter (pin Large=8). Head must reload at 8pt // with Inter's intrinsics — substantially smaller than 14pt OD. simulateReload(); - ensure("v2 parse ok", loadXml(kXml)); + UNSCOPED_INFO("v2 parse ok"); + CHECK(loadXml(kXml)); LLSD ovr_b; ovr_b["Target"] = "Inter"; resolve(ovr_b); LLFontGL* font_b = reg.getFont(LLFontDescriptor("Target", "Large", 0)); - ensure("getFont under Inter override", font_b != nullptr); + UNSCOPED_INFO("getFont under Inter override"); + REQUIRE(font_b != nullptr); F32 asc_b = font_b->getAscenderHeight(); - ensure("Inter ascender > 0", asc_b > 0.f); + UNSCOPED_INFO("Inter ascender > 0"); + CHECK(asc_b > 0.f); // Inter at 8pt is comfortably smaller than OpenDyslexic at 14pt. // 60% threshold separates "pin swapped cleanly" from "stuck at // OD's 14pt" with margin against FreeType rounding noise. - ensure("ascender shrinks across override swap (OD 14pt -> Inter 8pt)", - asc_b < asc_a * 0.6f); + UNSCOPED_INFO("ascender shrinks across override swap (OD 14pt -> Inter 8pt)"); + CHECK(asc_b < asc_a * 0.6f); } // F-NULL1 regression: getFont for a descriptor whose font files all @@ -500,8 +519,7 @@ namespace // the next getFont silently returned the cached NULL and widgets // dereferenced it; the fix at llfontregistry.cpp:1555-1567 only // inserts on success. This test pins that contract. - template<> template<> - void llfontregistry_gl_object::test<8>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: a font whose files all fail to load is not cached as NULL", "[LLFontRegistry-GL]") { // No file presence check needed — the whole point is that the // file is missing. @@ -512,15 +530,17 @@ namespace " " " " ""; - ensure("parse ok", loadXml(kXml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(kXml)); resolve(); LLFontDescriptor desc("Phantom", "Small", 0); LLFontGL* first = reg.getFont(desc); - ensure("getFont with missing file returns NULL", first == nullptr); + UNSCOPED_INFO("getFont with missing file returns NULL"); + REQUIRE(first == nullptr); - ensure("failed createFont must NOT poison mFontMap with a NULL slot", - !fontMapContains(desc)); + UNSCOPED_INFO("failed createFont must NOT poison mFontMap with a NULL slot"); + CHECK_FALSE(fontMapContains(desc)); } // reloadForDpiChange walks heads + fallback cache and resets each @@ -529,31 +549,34 @@ namespace // (FreeType point→pixel conversion goes through DPI). Pins the // fast-DPI path that LLFontGL::initClass takes when fonts.xml + // overrides are unchanged but only the scale moved. - template<> template<> - void llfontregistry_gl_object::test<9>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: reloadForDpiChange at twice the DPI roughly doubles the ascender", "[LLFontRegistry-GL]") { if (!interFontPresent()) { - skip("InterVariable.woff2 not present in test data dir"); + SKIP("InterVariable.woff2 not present in test data dir"); return; } - ensure("parse ok", loadXml(kInterXml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(kInterXml)); LLFontGL* font = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - ensure("getFont resolves", font != nullptr); + UNSCOPED_INFO("getFont resolves"); + REQUIRE(font != nullptr); F32 ascender_96 = font->getAscenderHeight(); - ensure("ascender at 96 DPI > 0", ascender_96 > 0.f); + UNSCOPED_INFO("ascender at 96 DPI > 0"); + CHECK(ascender_96 > 0.f); LLFontGL::sVertDPI = 192.f; LLFontGL::sHorizDPI = 192.f; reg.reloadForDpiChange(); F32 ascender_192 = font->getAscenderHeight(); - ensure("ascender at 192 DPI > 0", ascender_192 > 0.f); + UNSCOPED_INFO("ascender at 192 DPI > 0"); + CHECK(ascender_192 > 0.f); // 192/96 = 2.0x; allow 1.7x lower bound for FreeType rounding. - ensure("ascender scales with DPI after reloadForDpiChange", - ascender_192 > ascender_96 * 1.7f); + UNSCOPED_INFO("ascender scales with DPI after reloadForDpiChange"); + CHECK(ascender_192 > ascender_96 * 1.7f); // Restore so neighbour tests see the fixture default. LLFontGL::sVertDPI = 96.f; @@ -566,29 +589,31 @@ namespace // must not drop in-use atlas pages on a short-interval invocation. // Pin: after generating ASCII glyphs, a sweep doesn't reduce page // count. - template<> template<> - void llfontregistry_gl_object::test<10>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: sweepGlyphCaches keeps in-use atlas pages", "[LLFontRegistry-GL]") { if (!interFontPresent()) { - skip("InterVariable.woff2 not present in test data dir"); + SKIP("InterVariable.woff2 not present in test data dir"); return; } - ensure("parse ok", loadXml(kInterXml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(kInterXml)); LLFontGL* font = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - ensure("getFont resolves", font != nullptr); + UNSCOPED_INFO("getFont resolves"); + REQUIRE(font != nullptr); font->generateASCIIglyphs(); const LLFontBitmapCache* cache = font->getFontFreetype()->getFontBitmapCache(); const U32 pages_before = cache->getNumBitmaps(EFontGlyphType::Grayscale); - ensure("at least one atlas page before sweep", pages_before >= 1u); + UNSCOPED_INFO("at least one atlas page before sweep"); + CHECK(pages_before >= 1u); reg.sweepGlyphCaches(); const U32 pages_after = cache->getNumBitmaps(EFontGlyphType::Grayscale); - ensure_equals("sweep does not drop in-use atlas pages", - pages_after, pages_before); + UNSCOPED_INFO("sweep does not drop in-use atlas pages"); + CHECK(pages_after == pages_before); } // End-to-end shared-fallback eviction: two heads built via the @@ -612,15 +637,14 @@ namespace // Post-fix: heads have no mGlyphInfoMap; lookup goes through the // face's findGlyphInfo, which misses post-eviction and falls through // to addShapedGlyphFromFont to re-rasterize on a fresh sheet. - template<> template<> - void llfontregistry_gl_object::test<11>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: heads sharing an evicted fallback re-rasterize onto a live sheet", "[LLFontRegistry-GL]") { const std::string emoji_path = std::string(kFontDir) + "Noto-COLRv1.ttf"; const std::string dejavu_path = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string inter_path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(emoji_path) || !fileExists(dejavu_path) || !fileExists(inter_path)) { - skip("Noto-COLRv1.ttf + DejaVuSans.woff2 + InterVariable.woff2 required"); + SKIP("Noto-COLRv1.ttf + DejaVuSans.woff2 + InterVariable.woff2 required"); return; } @@ -642,13 +666,16 @@ namespace " " " " ""; - ensure("parse ok", loadXml(kXml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(kXml)); resolve(); LLFontGL* head_a = reg.getFont(LLFontDescriptor("HeadA", "Small", 0)); LLFontGL* head_b = reg.getFont(LLFontDescriptor("HeadB", "Small", 0)); - ensure("HeadA resolves", head_a != nullptr); - ensure("HeadB resolves", head_b != nullptr); + UNSCOPED_INFO("HeadA resolves"); + REQUIRE(head_a != nullptr); + UNSCOPED_INFO("HeadB resolves"); + REQUIRE(head_b != nullptr); // Locate the Noto-COLRv1 fallback inside each head's chain. auto find_emoji_fallback = [](LLFontGL* head) -> const LLFontFreetype* @@ -662,11 +689,13 @@ namespace }; const LLFontFreetype* emoji_a = find_emoji_fallback(head_a); const LLFontFreetype* emoji_b = find_emoji_fallback(head_b); - ensure("HeadA chain includes Noto-COLRv1", emoji_a != nullptr); - ensure("HeadB chain includes Noto-COLRv1", emoji_b != nullptr); - ensure_equals("siblings share the fallback freetype instance " - "(mFallbackInstanceCache dedup)", - emoji_a, emoji_b); + UNSCOPED_INFO("HeadA chain includes Noto-COLRv1"); + REQUIRE(emoji_a != nullptr); + UNSCOPED_INFO("HeadB chain includes Noto-COLRv1"); + REQUIRE(emoji_b != nullptr); + UNSCOPED_INFO("siblings share the fallback freetype instance " + "(mFallbackInstanceCache dedup)"); + REQUIRE(emoji_a == emoji_b); // Rasterize an emoji glyph through both heads. Both routes go // through getGlyphInfo → cmap miss on the head face → fallback @@ -677,15 +706,20 @@ namespace kFire, EFontGlyphType::Color); const LLFontGlyphInfo* gi_b = head_b->getFontFreetype()->getGlyphInfo( kFire, EFontGlyphType::Color); - ensure("HeadA produced an emoji glyph entry", gi_a != nullptr); - ensure("HeadB produced an emoji glyph entry", gi_b != nullptr); + UNSCOPED_INFO("HeadA produced an emoji glyph entry"); + REQUIRE(gi_a != nullptr); + UNSCOPED_INFO("HeadB produced an emoji glyph entry"); + REQUIRE(gi_b != nullptr); // Face-level dedup → both heads see the same face-owned entry. - ensure_equals("face dedup returns the same entry to both heads", - gi_a, gi_b); + UNSCOPED_INFO("face dedup returns the same entry to both heads"); + REQUIRE(gi_a == gi_b); const auto target = gi_a->mPhaseSlots[0].mBitmapEntry; - ensure("emoji glyph references a real Color sheet", - target.first == EFontGlyphType::Color && target.second >= 0); + { + INFO("emoji glyph references a real Color sheet"); + CHECK(target.first == EFontGlyphType::Color); + CHECK(target.second >= 0); + } // Simulate the fallback's collectGarbage releasing this sheet. // Delete face-owned entries pointing at (Color, target.second), @@ -694,8 +728,10 @@ namespace // to the deleted entry; post-fix, neither head has such a cache. const ALFontFace* emoji_face = emoji_a->getFontFace(); LLFontBitmapCache* cache = emoji_a->getBitmapCache(); - ensure("fallback freetype exposes its face", emoji_face != nullptr); - ensure("fallback freetype exposes its atlas", cache != nullptr); + UNSCOPED_INFO("fallback freetype exposes its face"); + REQUIRE(emoji_face != nullptr); + UNSCOPED_INFO("fallback freetype exposes its atlas"); + REQUIRE(cache != nullptr); emoji_face->erase_glyph_entries( [target](const LLFontGlyphInfo* gi) @@ -709,8 +745,8 @@ namespace return false; }); cache->releaseSheet(target.first, static_cast(target.second)); - ensure("target sheet reports released", - cache->isSheetReleased(target.first, + UNSCOPED_INFO("target sheet reports released"); + CHECK(cache->isSheetReleased(target.first, static_cast(target.second))); // Re-request the emoji through both heads. Each lookup must: @@ -721,17 +757,17 @@ namespace kFire, EFontGlyphType::Color); const LLFontGlyphInfo* gi_b2 = head_b->getFontFreetype()->getGlyphInfo( kFire, EFontGlyphType::Color); - ensure("HeadA re-rasterized after sibling-driven eviction", - gi_a2 != nullptr); - ensure("HeadB re-rasterized after sibling-driven eviction", - gi_b2 != nullptr); + UNSCOPED_INFO("HeadA re-rasterized after sibling-driven eviction"); + REQUIRE(gi_a2 != nullptr); + UNSCOPED_INFO("HeadB re-rasterized after sibling-driven eviction"); + REQUIRE(gi_b2 != nullptr); const auto post_a = gi_a2->mPhaseSlots[0].mBitmapEntry; const auto post_b = gi_b2->mPhaseSlots[0].mBitmapEntry; - ensure("HeadA's post-eviction entry points at a live sheet", - !cache->isSheetReleased(post_a.first, + UNSCOPED_INFO("HeadA's post-eviction entry points at a live sheet"); + CHECK_FALSE(cache->isSheetReleased(post_a.first, static_cast(post_a.second))); - ensure("HeadB's post-eviction entry points at a live sheet", - !cache->isSheetReleased(post_b.first, + UNSCOPED_INFO("HeadB's post-eviction entry points at a live sheet"); + CHECK_FALSE(cache->isSheetReleased(post_b.first, static_cast(post_b.second))); } } diff --git a/indra/llrender/tests/llfontregistry_test.cpp b/indra/llrender/tests/llfontregistry_test.cpp index 76e8174079..10c0279bca 100644 --- a/indra/llrender/tests/llfontregistry_test.cpp +++ b/indra/llrender/tests/llfontregistry_test.cpp @@ -40,7 +40,7 @@ #include "llxmlnode.h" #include "llsd.h" -#include "../test/lltut.h" +#include #include @@ -163,130 +163,127 @@ namespace tut } }; - // 128 slots: TUT defaults to 50; we already have 50+ tests and want - // headroom before the silent-drop threshold bites again. - typedef test_group llfontregistry_test; - typedef llfontregistry_test::object llfontregistry_object; -namespace -{ - tut::llfontregistry_test llfontregistry_testcase("LLFontRegistry"); -} - // =================================================================== // Group 1: LLFontDescriptor (pure) // =================================================================== // normalize() pulls an embedded "Huge"/"Large"/etc. out of the name and // moves it into the size slot, leaving the family stem behind. - template<> template<> - void llfontregistry_object::test<1>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: normalize moves a size suffix like Huge from the name to the size", "[LLFontRegistry]") { LLFontDescriptor desc("SansSerifHuge", "", 0); LLFontDescriptor n = desc.normalize(); - ensure_equals("name keeps stem", n.getName(), std::string("SansSerif")); - ensure_equals("size becomes Huge", n.getSize(), std::string("Huge")); - ensure_equals("style stays NORMAL", (S32)n.getStyle(), 0); + UNSCOPED_INFO("name keeps stem"); + CHECK(n.getName() == std::string("SansSerif")); + UNSCOPED_INFO("size becomes Huge"); + CHECK(n.getSize() == std::string("Huge")); + UNSCOPED_INFO("style stays NORMAL"); + CHECK((S32)n.getStyle() == 0); } // "Big" is the legacy alias for "Large" — verify the rewrite happens. - template<> template<> - void llfontregistry_object::test<2>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: normalize rewrites the legacy size suffix Big to Large", "[LLFontRegistry]") { LLFontDescriptor n = LLFontDescriptor("SansSerifBig", "", 0).normalize(); - ensure_equals("Big maps to Large", n.getSize(), std::string("Large")); - ensure_equals("name stripped", n.getName(), std::string("SansSerif")); + UNSCOPED_INFO("Big maps to Large"); + CHECK(n.getSize() == std::string("Large")); + UNSCOPED_INFO("name stripped"); + CHECK(n.getName() == std::string("SansSerif")); } // "Bold"/"Italic" suffixes set the style flags AND get stripped from // the name (the leading inline comment in normalize() claims the name // is preserved, but the code definitively removes the substring). - template<> template<> - void llfontregistry_object::test<3>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: normalize strips a Bold suffix from the name and sets the BOLD flag", "[LLFontRegistry]") { LLFontDescriptor n = LLFontDescriptor("SansSerifBold", "", 0).normalize(); - ensure_equals("Bold stripped from name", - n.getName(), std::string("SansSerif")); - ensure_equals("size defaults to Small (no size suffix in input)", - n.getSize(), std::string("Small")); - ensure_equals("BOLD flag set", (S32)n.getStyle(), (S32)LLFontGL::BOLD); + UNSCOPED_INFO("Bold stripped from name"); + CHECK(n.getName() == std::string("SansSerif")); + UNSCOPED_INFO("size defaults to Small (no size suffix in input)"); + CHECK(n.getSize() == std::string("Small")); + UNSCOPED_INFO("BOLD flag set"); + CHECK((S32)n.getStyle() == (S32)LLFontGL::BOLD); } // Bold + Italic combine; other style bits (UNDERLINE) get filtered. - template<> template<> - void llfontregistry_object::test<4>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: normalize combines Bold and Italic and drops the UNDERLINE bit", "[LLFontRegistry]") { LLFontDescriptor desc("SansSerifBoldItalic", "", static_cast(LLFontGL::UNDERLINE)); LLFontDescriptor n = desc.normalize(); - ensure_equals("BOLD|ITALIC flags", - (S32)n.getStyle(), + UNSCOPED_INFO("BOLD|ITALIC flags"); + CHECK((S32)n.getStyle() == (S32)(LLFontGL::BOLD | LLFontGL::ITALIC)); } // "Monospace" is preserved as both name and size — the special-case // branch in normalize() exists because stripping it would leave "". - template<> template<> - void llfontregistry_object::test<5>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: normalize keeps Monospace as both the name and the size", "[LLFontRegistry]") { LLFontDescriptor n = LLFontDescriptor("Monospace", "", 0).normalize(); - ensure_equals("name preserved", n.getName(), std::string("Monospace")); - ensure_equals("size becomes Monospace", n.getSize(), std::string("Monospace")); + UNSCOPED_INFO("name preserved"); + CHECK(n.getName() == std::string("Monospace")); + UNSCOPED_INFO("size becomes Monospace"); + CHECK(n.getSize() == std::string("Monospace")); } // Empty size + non-Monospace name → "Small" default. - template<> template<> - void llfontregistry_object::test<6>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: normalize defaults an empty size to Small", "[LLFontRegistry]") { LLFontDescriptor n = LLFontDescriptor("SansSerif", "", 0).normalize(); - ensure_equals("default size is Small", n.getSize(), std::string("Small")); + UNSCOPED_INFO("default size is Small"); + CHECK(n.getSize() == std::string("Small")); } // Files attached to a descriptor survive the normalize() round-trip. - template<> template<> - void llfontregistry_object::test<7>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: normalize keeps the descriptor's font files", "[LLFontRegistry]") { LLFontDescriptor desc("SansSerif", "", 0); desc.addFontFile("Foo.ttf", EFontHinting::DEFAULT, 0, 0.f); LLFontDescriptor n = desc.normalize(); - ensure_equals("file count preserved", (S32)n.getFontFiles().size(), 1); - ensure_equals("file name preserved", - n.getFontFiles()[0].FileName, std::string("Foo.ttf")); + UNSCOPED_INFO("file count preserved"); + REQUIRE((S32)n.getFontFiles().size() == 1); + UNSCOPED_INFO("file name preserved"); + CHECK(n.getFontFiles()[0].FileName == std::string("Foo.ttf")); } // operator== compares (name, style, size). Different files but same // identity-tuple still compare equal — that's intentional, the file // list is treated as data the descriptor carries, not part of identity. - template<> template<> - void llfontregistry_object::test<8>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: operator== and hash_value ignore the font file list", "[LLFontRegistry]") { LLFontDescriptor a("SansSerif", "Medium", 0); LLFontDescriptor b("SansSerif", "Medium", 0); a.addFontFile("a.ttf", EFontHinting::DEFAULT, 0, 0.f); b.addFontFile("b.ttf", EFontHinting::DEFAULT, 0, 0.f); - ensure("equal regardless of files", a == b); - ensure_equals("hash matches", hash_value(a), hash_value(b)); + UNSCOPED_INFO("equal regardless of files"); + CHECK(a == b); + UNSCOPED_INFO("hash matches"); + CHECK(hash_value(a) == hash_value(b)); } // operator< orders lexicographically by (name, style, size). Style // comparisons happen ahead of size — verify by holding name fixed. - template<> template<> - void llfontregistry_object::test<9>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: operator< orders descriptors that differ only in style", "[LLFontRegistry]") { LLFontDescriptor a("SansSerif", "Medium", 0); LLFontDescriptor b("SansSerif", "Medium", LLFontGL::BOLD); - ensure("style breaks tie before size", a < b); - ensure("strict weak ordering", !(b < a)); + UNSCOPED_INFO("style breaks tie before size"); + CHECK(a < b); + UNSCOPED_INFO("strict weak ordering"); + CHECK_FALSE(b < a); } // The "TEMPLATE" sentinel marks descriptors that exist only as the // post-XML template (no concrete point size yet). - template<> template<> - void llfontregistry_object::test<10>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: isTemplate is true only for the TEMPLATE size sentinel", "[LLFontRegistry]") { LLFontDescriptor t("SansSerif", "TEMPLATE", 0); LLFontDescriptor n("SansSerif", "Medium", 0); - ensure("template sentinel detected", t.isTemplate()); - ensure("non-template not flagged", !n.isTemplate()); + UNSCOPED_INFO("template sentinel detected"); + CHECK(t.isTemplate()); + UNSCOPED_INFO("non-template not flagged"); + CHECK_FALSE(n.isTemplate()); } // =================================================================== @@ -295,28 +292,32 @@ namespace // populates the global size table; nameToSize hits it // when no per-family override applies. - template<> template<> - void llfontregistry_object::test<11>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: fills the global table that nameToSize reads", "[LLFontRegistry]") { const char* xml = "" " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); F32 sz = 0.f; - ensure("Medium found", reg.nameToSize("Medium", sz)); - ensure_equals("Medium = 12", sz, 12.0f); - ensure("Large found", reg.nameToSize("Large", sz)); - ensure_equals("Large = 14", sz, 14.0f); - ensure("Huge missing", !reg.nameToSize("Huge", sz)); + UNSCOPED_INFO("Medium found"); + CHECK(reg.nameToSize("Medium", sz)); + UNSCOPED_INFO("Medium = 12"); + CHECK(sz == 12.0f); + UNSCOPED_INFO("Large found"); + CHECK(reg.nameToSize("Large", sz)); + UNSCOPED_INFO("Large = 14"); + CHECK(sz == 14.0f); + UNSCOPED_INFO("Huge missing"); + CHECK_FALSE(reg.nameToSize("Huge", sz)); } // New-format " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); const LLFontDescriptor* normal = templateFor("Inter", LLFontGL::NORMAL); const LLFontDescriptor* bold = templateFor("Inter", LLFontGL::BOLD); - ensure("NORMAL template present", normal != nullptr); - ensure("BOLD template present", bold != nullptr); - ensure("NORMAL inherits family wght set", - normal->getFontFiles()[0].mVarAxes.wght_set); - ensure_equals("NORMAL inherits family weight 400", - normal->getFontFiles()[0].mVarAxes.wght, 400.f); - ensure_equals("NORMAL inherits family hinting light", - (S32)normal->getFontFiles()[0].mHinting, + UNSCOPED_INFO("NORMAL template present"); + REQUIRE(normal != nullptr); + UNSCOPED_INFO("BOLD template present"); + REQUIRE(bold != nullptr); + UNSCOPED_INFO("NORMAL inherits family wght set"); + CHECK(normal->getFontFiles()[0].mVarAxes.wght_set); + UNSCOPED_INFO("NORMAL inherits family weight 400"); + CHECK(normal->getFontFiles()[0].mVarAxes.wght == 400.f); + UNSCOPED_INFO("NORMAL inherits family hinting light"); + CHECK((S32)normal->getFontFiles()[0].mHinting == (S32)EFontHinting::LIGHT); - ensure("BOLD has wght set after override", - bold->getFontFiles()[0].mVarAxes.wght_set); - ensure_equals("BOLD overrides weight to 700", - bold->getFontFiles()[0].mVarAxes.wght, 700.f); - ensure_equals("BOLD inherits family hinting", - (S32)bold->getFontFiles()[0].mHinting, + UNSCOPED_INFO("BOLD has wght set after override"); + CHECK(bold->getFontFiles()[0].mVarAxes.wght_set); + UNSCOPED_INFO("BOLD overrides weight to 700"); + CHECK(bold->getFontFiles()[0].mVarAxes.wght == 700.f); + UNSCOPED_INFO("BOLD inherits family hinting"); + CHECK((S32)bold->getFontFiles()[0].mHinting == (S32)EFontHinting::LIGHT); } // children inside populate the per-family override table // that nameToSize(family, size) consults before the global table. - template<> template<> - void llfontregistry_object::test<13>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a family's overrides the global size for that family only", "[LLFontRegistry]") { const char* xml = "" @@ -363,22 +366,24 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); F32 sz = 0.f; - ensure("DejaVu Medium hits family override", - reg.nameToSize("DejaVu", "Medium", sz)); - ensure_equals("DejaVu Medium = 8.6", sz, 8.6f); + UNSCOPED_INFO("DejaVu Medium hits family override"); + CHECK(reg.nameToSize("DejaVu", "Medium", sz)); + UNSCOPED_INFO("DejaVu Medium = 8.6"); + CHECK(sz == 8.6f); F32 sz2 = 0.f; - ensure("Other family Medium falls back to global", - reg.nameToSize("SansSerif", "Medium", sz2)); - ensure_equals("Global Medium unchanged", sz2, 12.0f); + UNSCOPED_INFO("Other family Medium falls back to global"); + CHECK(reg.nameToSize("SansSerif", "Medium", sz2)); + UNSCOPED_INFO("Global Medium unchanged"); + CHECK(sz2 == 12.0f); } // Family-level ligatures="on" propagates to every file unless the // file overrides. The flag lives on LLFontFileInfo (it controls the // freetype-level setAllowMonospaceLigatures call later on). - template<> template<> - void llfontregistry_object::test<14>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a family's ligatures=on carries to its files", "[LLFontRegistry]") { const char* xml = "" @@ -386,17 +391,18 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); const LLFontDescriptor* d = templateFor("Mono", LLFontGL::NORMAL); - ensure("template present", d != nullptr); - ensure("ligatures inherited", - d->getFontFiles()[0].mMonospaceLigatures); + UNSCOPED_INFO("template present"); + REQUIRE(d != nullptr); + UNSCOPED_INFO("ligatures inherited"); + CHECK(d->getFontFiles()[0].mMonospaceLigatures); } // load_collection on the family is inherited by every file; file-level // load_collection overrides per-file. Verify both paths. - template<> template<> - void llfontregistry_object::test<15>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: load_collection works on the family and on a single file", "[LLFontRegistry]") { const char* xml = "" @@ -407,18 +413,18 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); - ensure("family-level load_collection inherited", - templateFor("CJK")->getFontFiles()[0].mLoadCollection); - ensure("file-level load_collection works alone", - templateFor("Mono")->getFontFiles()[0].mLoadCollection); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); + UNSCOPED_INFO("family-level load_collection inherited"); + CHECK(templateFor("CJK")->getFontFiles()[0].mLoadCollection); + UNSCOPED_INFO("file-level load_collection works alone"); + CHECK(templateFor("Mono")->getFontFiles()[0].mLoadCollection); } // flags="bold" forces LLFontGL::BOLD into the file's flags so that // a single .woff2 carrying multiple weights still lights up callers' // BOLD checks. - template<> template<> - void llfontregistry_object::test<16>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a file's flags=bold sets BOLD in its flags", "[LLFontRegistry]") { const char* xml = "" @@ -428,18 +434,19 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); const LLFontDescriptor* d = templateFor("Inter", LLFontGL::BOLD); - ensure("template present", d != nullptr); - ensure_equals("BOLD bit set on file flags", - d->getFontFiles()[0].mFlags & LLFontGL::BOLD, + UNSCOPED_INFO("template present"); + REQUIRE(d != nullptr); + UNSCOPED_INFO("BOLD bit set on file flags"); + CHECK((d->getFontFiles()[0].mFlags & LLFontGL::BOLD) == (S32)LLFontGL::BOLD); } // unicode_ranges parses comma-separated U+lo-U+hi tokens into a // CharFunctor that returns true for matching codepoints. - template<> template<> - void llfontregistry_object::test<17>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: unicode_ranges builds a CharFunctor from U+ ranges and code points", "[LLFontRegistry]") { const char* xml = "" @@ -449,21 +456,28 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); const auto& cf = templateFor("Emoji")->getFontFiles()[0].CharFunctor; - ensure("functor present", static_cast(cf)); - ensure("U+2600 inside range", cf(0x2600)); - ensure("U+27BF inside range", cf(0x27BF)); - ensure("U+27C0 outside range", !cf(0x27C0)); - ensure("U+1F525 single CP", cf(0x1F525)); - ensure("0x200D via 0x prefix", cf(0x200D)); - ensure("U+0000 nowhere covered", !cf(0)); + UNSCOPED_INFO("functor present"); + REQUIRE(static_cast(cf)); + UNSCOPED_INFO("U+2600 inside range"); + CHECK(cf(0x2600)); + UNSCOPED_INFO("U+27BF inside range"); + CHECK(cf(0x27BF)); + UNSCOPED_INFO("U+27C0 outside range"); + CHECK_FALSE(cf(0x27C0)); + UNSCOPED_INFO("U+1F525 single CP"); + CHECK(cf(0x1F525)); + UNSCOPED_INFO("0x200D via 0x prefix"); + CHECK(cf(0x200D)); + UNSCOPED_INFO("U+0000 nowhere covered"); + CHECK_FALSE(cf(0)); } // Malformed unicode_ranges tokens get skipped with a log, but the // remaining valid tokens still build a functor. - template<> template<> - void llfontregistry_object::test<18>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: unicode_ranges skips malformed tokens and keeps the valid ones", "[LLFontRegistry]") { const char* xml = "" @@ -473,16 +487,18 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); const auto& cf = templateFor("Frag")->getFontFiles()[0].CharFunctor; - ensure("functor still built", static_cast(cf)); - ensure("valid range survives", cf(0x2700)); + UNSCOPED_INFO("functor still built"); + REQUIRE(static_cast(cf)); + UNSCOPED_INFO("valid range survives"); + CHECK(cf(0x2700)); } // size_delta carries to the file unchanged. createFont adds it to // the resolved point size when loading a face. - template<> template<> - void llfontregistry_object::test<19>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: size_delta carries to the file unchanged", "[LLFontRegistry]") { const char* xml = "" @@ -492,15 +508,15 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); - ensure_equals("size_delta preserved", - templateFor("X")->getFontFiles()[0].mSizeDelta, -1.5f); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); + UNSCOPED_INFO("size_delta preserved"); + CHECK(templateFor("X")->getFontFiles()[0].mSizeDelta == -1.5f); } // Family metadata: label / user_selectable / monospace flow into // mFamilyMeta where getAvailableFamilies reads them. - template<> template<> - void llfontregistry_object::test<20>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: getAvailableFamilies reports labels and hides user_selectable=false", "[LLFontRegistry]") { const char* xml = "" @@ -514,25 +530,28 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); auto fams = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::ANY); // Hidden filtered out; Inter + Mono remain. Stable sort by label. - ensure_equals("Hidden filtered + 2 visible families", (S32)fams.size(), 2); + UNSCOPED_INFO("Hidden filtered + 2 visible families"); + REQUIRE((S32)fams.size() == 2); bool sawInter = false, sawMono = false; for (const auto& f : fams) { - if (f.name == "Inter") { sawInter = true; ensure_equals("Inter label", f.label, std::string("Inter Display")); } + if (f.name == "Inter") { sawInter = true; UNSCOPED_INFO("Inter label"); REQUIRE(f.label == std::string("Inter Display")); } if (f.name == "Mono") { sawMono = true; } } - ensure("Inter present", sawInter); - ensure("Mono present", sawMono); + UNSCOPED_INFO("Inter present"); + CHECK(sawInter); + UNSCOPED_INFO("Mono present"); + CHECK(sawMono); } // Two consecutive init_from_xml calls (mimicking cross-skin layering) // PREPEND the second layer's files onto an existing same-key entry. - template<> template<> - void llfontregistry_object::test<21>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a second init_from_xml layer prepends its files to the family", "[LLFontRegistry]") { const char* base = "" @@ -546,12 +565,17 @@ namespace " " " " ""; - ensure("base parse", loadXml(base)); - ensure("overlay parse", loadXml(overlay)); + UNSCOPED_INFO("base parse"); + CHECK(loadXml(base)); + UNSCOPED_INFO("overlay parse"); + CHECK(loadXml(overlay)); auto names = fileNames("SansSerif"); - ensure_equals("two files merged", (S32)names.size(), 2); - ensure_equals("overlay prepended", names[0], std::string("Overlay.woff2")); - ensure_equals("base kept as fallback", names[1], std::string("Base.woff2")); + UNSCOPED_INFO("two files merged"); + REQUIRE((S32)names.size() == 2); + UNSCOPED_INFO("overlay prepended"); + CHECK(names[0] == std::string("Overlay.woff2")); + UNSCOPED_INFO("base kept as fallback"); + CHECK(names[1] == std::string("Base.woff2")); } // =================================================================== @@ -560,8 +584,7 @@ namespace // Simple appends B's files onto A's chain at the // matching style. - template<> template<> - void llfontregistry_object::test<22>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: appends the used family's files after the family's own", "[LLFontRegistry]") { const char* xml = "" @@ -573,18 +596,21 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); auto names = fileNames("A"); - ensure_equals("A then B", (S32)names.size(), 2); - ensure_equals("A own first", names[0], std::string("A.woff2")); - ensure_equals("B appended", names[1], std::string("B.woff2")); + UNSCOPED_INFO("A then B"); + REQUIRE((S32)names.size() == 2); + UNSCOPED_INFO("A own first"); + CHECK(names[0] == std::string("A.woff2")); + UNSCOPED_INFO("B appended"); + CHECK(names[1] == std::string("B.woff2")); } // Style fallback: A wants B at BOLD, B only declares NORMAL. // Resolver should pull B's NORMAL files into A's BOLD chain. - template<> template<> - void llfontregistry_object::test<23>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: of a family without BOLD fills the BOLD chain from its NORMAL", "[LLFontRegistry]") { const char* xml = "" @@ -596,19 +622,22 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); auto names = fileNames("A", LLFontGL::BOLD); - ensure_equals("A-Bold then B-Regular fallback", (S32)names.size(), 2); - ensure_equals("A's bold first", names[0], std::string("A-Bold.woff2")); - ensure_equals("B NORMAL fallback", names[1], std::string("B-Regular.woff2")); + UNSCOPED_INFO("A-Bold then B-Regular fallback"); + REQUIRE((S32)names.size() == 2); + UNSCOPED_INFO("A's bold first"); + CHECK(names[0] == std::string("A-Bold.woff2")); + UNSCOPED_INFO("B NORMAL fallback"); + CHECK(names[1] == std::string("B-Regular.woff2")); } // Cycle A→B + B→A: each family's chain ends with the other's files // exactly once. Without the visited-set guard this would recurse // infinitely or duplicate. - template<> template<> - void llfontregistry_object::test<24>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a two-family cycle puts each file on each chain once", "[LLFontRegistry]") { const char* xml = "" @@ -621,18 +650,22 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); - ensure_equals("A.woff2 once on A", countFile("A", "A.woff2"), 1); - ensure_equals("B.woff2 once on A", countFile("A", "B.woff2"), 1); - ensure_equals("B.woff2 once on B", countFile("B", "B.woff2"), 1); - ensure_equals("A.woff2 once on B", countFile("B", "A.woff2"), 1); + UNSCOPED_INFO("A.woff2 once on A"); + CHECK(countFile("A", "A.woff2") == 1); + UNSCOPED_INFO("B.woff2 once on A"); + CHECK(countFile("A", "B.woff2") == 1); + UNSCOPED_INFO("B.woff2 once on B"); + CHECK(countFile("B", "B.woff2") == 1); + UNSCOPED_INFO("A.woff2 once on B"); + CHECK(countFile("B", "A.woff2") == 1); } // Diamond A→B, A→C, B→D, C→D: D must appear exactly once on A's // chain — the visited set is shared across the whole walk. - template<> template<> - void llfontregistry_object::test<25>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a diamond puts the shared family on the chain once", "[LLFontRegistry]") { const char* xml = "" @@ -653,17 +686,19 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); - ensure_equals("D.woff2 appears exactly once on A", - countFile("A", "D.woff2"), 1); - ensure_equals("B.woff2 once on A", countFile("A", "B.woff2"), 1); - ensure_equals("C.woff2 once on A", countFile("A", "C.woff2"), 1); + UNSCOPED_INFO("D.woff2 appears exactly once on A"); + CHECK(countFile("A", "D.woff2") == 1); + UNSCOPED_INFO("B.woff2 once on A"); + CHECK(countFile("A", "B.woff2") == 1); + UNSCOPED_INFO("C.woff2 once on A"); + CHECK(countFile("A", "C.woff2") == 1); } // Self-reference logs and is skipped. - template<> template<> - void llfontregistry_object::test<26>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a of the family itself is skipped", "[LLFontRegistry]") { const char* xml = "" @@ -672,17 +707,17 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); - ensure_equals("only A's own file remains", - countFile("A", "A.woff2"), 1); - ensure_equals("nothing else added", - (S32)fileNames("A").size(), 1); + UNSCOPED_INFO("only A's own file remains"); + CHECK(countFile("A", "A.woff2") == 1); + UNSCOPED_INFO("nothing else added"); + REQUIRE((S32)fileNames("A").size() == 1); } // targeting an undeclared family is skipped. - template<> template<> - void llfontregistry_object::test<27>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a of an undeclared family is skipped", "[LLFontRegistry]") { const char* xml = "" @@ -691,16 +726,16 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); - ensure_equals("undeclared use skipped", - (S32)fileNames("A").size(), 1); + UNSCOPED_INFO("undeclared use skipped"); + REQUIRE((S32)fileNames("A").size() == 1); } // -only family (no " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); - ensure("Wrapper NORMAL synthesized", - templateFor("Wrapper", LLFontGL::NORMAL) != nullptr); - ensure("Wrapper BOLD synthesized", - templateFor("Wrapper", LLFontGL::BOLD) != nullptr); + UNSCOPED_INFO("Wrapper NORMAL synthesized"); + REQUIRE(templateFor("Wrapper", LLFontGL::NORMAL) != nullptr); + UNSCOPED_INFO("Wrapper BOLD synthesized"); + REQUIRE(templateFor("Wrapper", LLFontGL::BOLD) != nullptr); auto bold = fileNames("Wrapper", LLFontGL::BOLD); - ensure_equals("Wrapper BOLD pulls Inner-B", (S32)bold.size(), 1); - ensure_equals("...and the file is Inner-B", - bold[0], std::string("Inner-B.woff2")); + UNSCOPED_INFO("Wrapper BOLD pulls Inner-B"); + REQUIRE((S32)bold.size() == 1); + UNSCOPED_INFO("...and the file is Inner-B"); + CHECK(bold[0] == std::string("Inner-B.woff2")); } // inherit="true" on BOLD appends the family's NORMAL files. // Verify: BOLD's chain ends with NORMAL's contents. - template<> template<> - void llfontregistry_object::test<29>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: inherit=true on BOLD appends the family's NORMAL files", "[LLFontRegistry]") { const char* xml = "" @@ -736,12 +772,16 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); auto names = fileNames("F", LLFontGL::BOLD); - ensure_equals("BOLD then NORMAL via inherit", (S32)names.size(), 2); - ensure_equals("BOLD first", names[0], std::string("F-B.woff2")); - ensure_equals("NORMAL appended", names[1], std::string("F-R.woff2")); + UNSCOPED_INFO("BOLD then NORMAL via inherit"); + REQUIRE((S32)names.size() == 2); + UNSCOPED_INFO("BOLD first"); + CHECK(names[0] == std::string("F-B.woff2")); + UNSCOPED_INFO("NORMAL appended"); + CHECK(names[1] == std::string("F-R.woff2")); } // applies per-style before inherit; then inherit appends NORMAL's @@ -751,8 +791,7 @@ namespace // resolveFontReferences drops the second B because chain[0..2] already // covers everything the duplicate would; it just wastes the renderer's // per-codepoint chain walk to look up a face it'll handle the same way. - template<> template<> - void llfontregistry_object::test<30>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: BOLD inherit after keeps one copy of the used family's file", "[LLFontRegistry]") { const char* xml = "" @@ -765,24 +804,27 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); auto bold = fileNames("F", LLFontGL::BOLD); - ensure_equals("3 files post-dedup: own bold + B from + NORMAL inherit", - (S32)bold.size(), 3); - ensure_equals("F-B first", bold[0], std::string("F-B.woff2")); - ensure_equals("B from ", bold[1], std::string("B.woff2")); - ensure_equals("F-R inherited", bold[2], std::string("F-R.woff2")); - ensure_equals("duplicate B from NORMAL's deduped out", - countFile("F", "B.woff2", LLFontGL::BOLD), 1); + UNSCOPED_INFO("3 files post-dedup: own bold + B from + NORMAL inherit"); + REQUIRE((S32)bold.size() == 3); + UNSCOPED_INFO("F-B first"); + CHECK(bold[0] == std::string("F-B.woff2")); + UNSCOPED_INFO("B from "); + CHECK(bold[1] == std::string("B.woff2")); + UNSCOPED_INFO("F-R inherited"); + CHECK(bold[2] == std::string("F-R.woff2")); + UNSCOPED_INFO("duplicate B from NORMAL's deduped out"); + CHECK(countFile("F", "B.woff2", LLFontGL::BOLD) == 1); } // Calling resolve() a second time is a no-op — mFamilyUses / // mInheritFlags are consumed at the end of the first call. Per- // family sizes (mFamilySizes) and per-file source-family tags // survive — those are runtime data, not parse state. - template<> template<> - void llfontregistry_object::test<31>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a second resolveFontReferences call adds nothing", "[LLFontRegistry]") { const char* xml = "" @@ -794,13 +836,14 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); auto first = fileNames("A"); resolve(); // second call must not re-append auto second = fileNames("A"); - ensure_equals("size unchanged across re-resolve", - (S32)first.size(), (S32)second.size()); + UNSCOPED_INFO("size unchanged across re-resolve"); + REQUIRE((S32)first.size() == (S32)second.size()); } // =================================================================== @@ -810,8 +853,7 @@ namespace // Family→family override: target's chain becomes [source files..., // target's own files] so source wins for the head face but target // fallbacks remain available behind it. - template<> template<> - void llfontregistry_object::test<32>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a family override puts the source's files ahead of the target's", "[LLFontRegistry]") { const char* xml = "" @@ -822,23 +864,26 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); LLSD overrides; overrides["UI"] = "Alt"; applyOverrides(overrides); auto names = fileNames("UI"); - ensure_equals("source prepended + target preserved", (S32)names.size(), 2); - ensure_equals("Alt prepended", names[0], std::string("Alt.woff2")); - ensure_equals("UI fallback kept", names[1], std::string("UI.woff2")); + UNSCOPED_INFO("source prepended + target preserved"); + REQUIRE((S32)names.size() == 2); + UNSCOPED_INFO("Alt prepended"); + CHECK(names[0] == std::string("Alt.woff2")); + UNSCOPED_INFO("UI fallback kept"); + CHECK(names[1] == std::string("UI.woff2")); } // Family→file override: the override value isn't a known family, so // it's treated as a literal file path. The synthesized LLFontFileInfo // inherits hinting/weight/flags from the target's first file so the // swap behaves like a drop-in replacement. - template<> template<> - void llfontregistry_object::test<33>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a file override copies hinting/weight/ligatures from the target", "[LLFontRegistry]") { const char* xml = "" @@ -846,24 +891,27 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); LLSD overrides; overrides["UI"] = "user-supplied.ttf"; applyOverrides(overrides); const auto& files = templateFor("UI")->getFontFiles(); - ensure_equals("file added in front", files[0].FileName, + UNSCOPED_INFO("file added in front"); + CHECK(files[0].FileName == std::string("user-supplied.ttf")); - ensure_equals("hinting inherited from target's first file", - (S32)files[0].mHinting, (S32)EFontHinting::LIGHT); - ensure_equals("weight inherited", files[0].mVarAxes.wght, 400.f); - ensure("ligatures inherited", files[0].mMonospaceLigatures); + UNSCOPED_INFO("hinting inherited from target's first file"); + CHECK((S32)files[0].mHinting == (S32)EFontHinting::LIGHT); + UNSCOPED_INFO("weight inherited"); + CHECK(files[0].mVarAxes.wght == 400.f); + UNSCOPED_INFO("ligatures inherited"); + CHECK(files[0].mMonospaceLigatures); } // Self-override (target == source string) is rejected — the override // would otherwise prepend the target's own files onto itself. - template<> template<> - void llfontregistry_object::test<34>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an override of a family to itself is ignored", "[LLFontRegistry]") { const char* xml = "" @@ -871,18 +919,19 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); LLSD overrides; overrides["UI"] = "UI"; applyOverrides(overrides); auto names = fileNames("UI"); - ensure_equals("self-override skipped", (S32)names.size(), 1); + UNSCOPED_INFO("self-override skipped"); + REQUIRE((S32)names.size() == 1); } // Unknown target family: skipped, no phantom entries created. - template<> template<> - void llfontregistry_object::test<35>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an override of an undeclared family creates no template", "[LLFontRegistry]") { const char* xml = "" @@ -890,36 +939,39 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); LLSD overrides; overrides["Phantom"] = "Real"; applyOverrides(overrides); - ensure("no template materialized for Phantom", - templateFor("Phantom") == nullptr); - ensure_equals("Real untouched", - (S32)fileNames("Real").size(), 1); + UNSCOPED_INFO("no template materialized for Phantom"); + REQUIRE(templateFor("Phantom") == nullptr); + UNSCOPED_INFO("Real untouched"); + REQUIRE((S32)fileNames("Real").size() == 1); } // overridesEqual: snapshot of last-applied overrides updates with // each call to resolveFontReferences (which calls applyFamilyOverrides // and stashes the LLSD). - template<> template<> - void llfontregistry_object::test<36>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: overridesEqual compares against the overrides last resolved", "[LLFontRegistry]") { const char* xml = "" " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); LLSD ovr1; ovr1["UI"] = "Alt"; resolve(ovr1); - ensure("identical maps compare equal", reg.overridesEqual(ovr1)); + UNSCOPED_INFO("identical maps compare equal"); + CHECK(reg.overridesEqual(ovr1)); LLSD ovr2; ovr2["UI"] = "Different"; - ensure("different value differs", !reg.overridesEqual(ovr2)); + UNSCOPED_INFO("different value differs"); + CHECK_FALSE(reg.overridesEqual(ovr2)); } // applyOverrides prepends source files onto the target's chain. The @@ -929,8 +981,7 @@ namespace // own (or global) is what nameToSize returns. This pins the // post-redesign behavior: per-family size is absolute for THAT family // only, never propagated cross-family. - template<> template<> - void llfontregistry_object::test<37>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: override files carry the source family while nameToSize stays put", "[LLFontRegistry]") { const char* xml = "" @@ -943,7 +994,8 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); LLSD overrides; overrides["UI"] = "Alt"; @@ -951,30 +1003,31 @@ namespace // nameToSize for UI returns UI's own pin; the override does NOT // route this lookup through Alt. F32 sz = 0.f; - ensure(reg.nameToSize("UI", "Custom", sz)); - ensure_equals("UI Custom = own 9.5 (override does NOT route nameToSize)", - sz, 9.5f); + CHECK(reg.nameToSize("UI", "Custom", sz)); + UNSCOPED_INFO("UI Custom = own 9.5 (override does NOT route nameToSize)"); + CHECK(sz == 9.5f); // The override DID prepend Alt's file onto UI's chain, with // mSourceFamily=Alt — so per-file pin lookup at render time // would use Alt's for that prepended file. const auto& files = templateFor("UI")->getFontFiles(); - ensure("UI chain has at least one file", !files.empty()); - ensure_equals("first file = Alt's prepended file", - files[0].FileName, std::string("A.woff2")); - ensure_equals("prepended file tagged with source family Alt", - files[0].mSourceFamily, std::string("Alt")); + UNSCOPED_INFO("UI chain has at least one file"); + REQUIRE_FALSE(files.empty()); + UNSCOPED_INFO("first file = Alt's prepended file"); + CHECK(files[0].FileName == std::string("A.woff2")); + UNSCOPED_INFO("prepended file tagged with source family Alt"); + CHECK(files[0].mSourceFamily == std::string("Alt")); // Alt's pin is intact for per-file lookup. F32 alt_pin = 0.f; - ensure(familySizePt("Alt", "Custom", alt_pin)); - ensure_equals("Alt Custom pin = 11.5 (createFont uses this for Alt's file)", - alt_pin, 11.5f); + CHECK(familySizePt("Alt", "Custom", alt_pin)); + UNSCOPED_INFO("Alt Custom pin = 11.5 (createFont uses this for Alt's file)"); + CHECK(alt_pin == 11.5f); // Clear the override; UI's own pin still applies. applyOverrides(LLSD()); F32 sz2 = 0.f; - ensure(reg.nameToSize("UI", "Custom", sz2)); - ensure_equals("UI Custom = own 9.5 (override cleared)", - sz2, 9.5f); + CHECK(reg.nameToSize("UI", "Custom", sz2)); + UNSCOPED_INFO("UI Custom = own 9.5 (override cleared)"); + CHECK(sz2 == 9.5f); } // =================================================================== @@ -983,8 +1036,7 @@ namespace // getMatchingFontDesc applies normalize() to its argument, so passing // an unnormalized "SansSerifHuge" finds the {SansSerif, Huge} template. - template<> template<> - void llfontregistry_object::test<38>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: getMatchingFontDesc finds a declared template and misses an absent one", "[LLFontRegistry]") { const char* xml = "" @@ -992,18 +1044,20 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); LLFontDescriptor query("SansSerif", "TEMPLATE", 0); - ensure("template lookup hits", reg.getMatchingFontDesc(query) != nullptr); + UNSCOPED_INFO("template lookup hits"); + REQUIRE(reg.getMatchingFontDesc(query) != nullptr); LLFontDescriptor miss("Nope", "TEMPLATE", 0); - ensure("absent name misses", reg.getMatchingFontDesc(miss) == nullptr); + UNSCOPED_INFO("absent name misses"); + REQUIRE(reg.getMatchingFontDesc(miss) == nullptr); } // getClosestFontTemplate prefers the entry whose style bits overlap // most with the requested style, with BOLD as a tiebreaker. - template<> template<> - void llfontregistry_object::test<39>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: getClosestFontTemplate takes an exact style and breaks ties toward BOLD", "[LLFontRegistry]") { const char* xml = "" @@ -1013,29 +1067,32 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); // Asking for BOLD: exact match wins. LLFontDescriptor want_bold("F", "TEMPLATE", LLFontGL::BOLD); const LLFontDescriptor* best = reg.getClosestFontTemplate(want_bold); - ensure("bold exact", best != nullptr); - ensure_equals("bold style", (S32)best->getStyle(), (S32)LLFontGL::BOLD); + UNSCOPED_INFO("bold exact"); + REQUIRE(best != nullptr); + UNSCOPED_INFO("bold style"); + CHECK((S32)best->getStyle() == (S32)LLFontGL::BOLD); // Asking for BOLD|ITALIC with no BOLD|ITALIC entry: the BOLD // tiebreaker should prefer BOLD over ITALIC at equal bit-count. LLFontDescriptor want_bi("F", "TEMPLATE", (U8)(LLFontGL::BOLD | LLFontGL::ITALIC)); best = reg.getClosestFontTemplate(want_bi); - ensure("BI fallback hit", best != nullptr); - ensure_equals("BOLD wins tiebreak", - (S32)best->getStyle(), (S32)LLFontGL::BOLD); + UNSCOPED_INFO("BI fallback hit"); + REQUIRE(best != nullptr); + UNSCOPED_INFO("BOLD wins tiebreak"); + CHECK((S32)best->getStyle() == (S32)LLFontGL::BOLD); } // getClosestFontTemplate REJECTS entries that have style bits the // request didn't ask for. Asking for NORMAL must not return a BOLD // template even if the family has only BOLD. - template<> template<> - void llfontregistry_object::test<40>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: getClosestFontTemplate never returns a style the request lacks", "[LLFontRegistry]") { const char* xml = "" @@ -1043,17 +1100,17 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); LLFontDescriptor want_normal("F", "TEMPLATE", 0); - ensure("no NORMAL match returned", - reg.getClosestFontTemplate(want_normal) == nullptr); + UNSCOPED_INFO("no NORMAL match returned"); + REQUIRE(reg.getClosestFontTemplate(want_normal) == nullptr); } // Empty-family overload of nameToSize skips the per-family path // entirely and only consults the global table. - template<> template<> - void llfontregistry_object::test<41>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: nameToSize without a family reads only the global table", "[LLFontRegistry]") { const char* xml = "" @@ -1063,14 +1120,17 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); F32 sz = 0.f; - ensure("empty-family overload finds Medium", - reg.nameToSize("Medium", sz)); - ensure_equals("...and gets the global value", sz, 12.0f); + UNSCOPED_INFO("empty-family overload finds Medium"); + CHECK(reg.nameToSize("Medium", sz)); + UNSCOPED_INFO("...and gets the global value"); + CHECK(sz == 12.0f); F32 sz2 = 0.f; reg.nameToSize("F", "Medium", sz2); - ensure_equals("family overload still gets per-family value", sz2, 9.0f); + UNSCOPED_INFO("family overload still gets per-family value"); + CHECK(sz2 == 9.0f); } // =================================================================== @@ -1079,8 +1139,7 @@ namespace // ANY filter returns both monospace and proportional, "default" and // user_selectable=false are excluded, and result is sorted by label. - template<> template<> - void llfontregistry_object::test<42>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: getAvailableFamilies ANY drops hidden and default and sorts by label", "[LLFontRegistry]") { const char* xml = "" @@ -1097,20 +1156,21 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); auto fams = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::ANY); - ensure_equals("only 2 selectable families", (S32)fams.size(), 2); - ensure_equals("sorted by label, Apple first", - fams[0].name, std::string("Apple")); - ensure_equals("Zebra second", - fams[1].name, std::string("Zebra")); + UNSCOPED_INFO("only 2 selectable families"); + REQUIRE((S32)fams.size() == 2); + UNSCOPED_INFO("sorted by label, Apple first"); + CHECK(fams[0].name == std::string("Apple")); + UNSCOPED_INFO("Zebra second"); + CHECK(fams[1].name == std::string("Zebra")); } // MONOSPACE filter returns only families with monospace="true"; // PROPORTIONAL is the inverse. - template<> template<> - void llfontregistry_object::test<43>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: getAvailableFamilies MONOSPACE and PROPORTIONAL split on monospace", "[LLFontRegistry]") { const char* xml = "" @@ -1121,22 +1181,24 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); auto mono = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::MONOSPACE); - ensure_equals("MONOSPACE: 1 family", (S32)mono.size(), 1); - ensure_equals("MONOSPACE returns Mono", - mono[0].name, std::string("Mono")); + UNSCOPED_INFO("MONOSPACE: 1 family"); + REQUIRE((S32)mono.size() == 1); + UNSCOPED_INFO("MONOSPACE returns Mono"); + CHECK(mono[0].name == std::string("Mono")); auto prop = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::PROPORTIONAL); - ensure_equals("PROPORTIONAL: 1 family", (S32)prop.size(), 1); - ensure_equals("PROPORTIONAL returns Sans", - prop[0].name, std::string("Sans")); + UNSCOPED_INFO("PROPORTIONAL: 1 family"); + REQUIRE((S32)prop.size() == 1); + UNSCOPED_INFO("PROPORTIONAL returns Sans"); + CHECK(prop[0].name == std::string("Sans")); } // Family without a label attribute defaults its UI label to the // family name. - template<> template<> - void llfontregistry_object::test<44>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a family without a label uses its name as the label", "[LLFontRegistry]") { const char* xml = "" @@ -1144,20 +1206,21 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); auto fams = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::ANY); - ensure_equals("one family", (S32)fams.size(), 1); - ensure_equals("label defaults to name", - fams[0].label, std::string("Plain")); + UNSCOPED_INFO("one family"); + REQUIRE((S32)fams.size() == 1); + UNSCOPED_INFO("label defaults to name"); + CHECK(fams[0].label == std::string("Plain")); } // inside — literal self-reference // by name, distinct from the 'self' alias path (test 26). The cycle // detector must skip both. The post-resolve chain should contain // only A's own files, with no duplicate insertion. - template<> template<> - void llfontregistry_object::test<45>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a naming the family itself leaves only the family's own file", "[LLFontRegistry]") { const char* xml = "" @@ -1166,13 +1229,14 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); auto names = fileNames("Mono"); - ensure_equals("self-reference by literal name doesn't duplicate files", - (S32)names.size(), 1); - ensure_equals("only the family's own file in chain", - names[0], std::string("M.woff2")); + UNSCOPED_INFO("self-reference by literal name doesn't duplicate files"); + REQUIRE((S32)names.size() == 1); + UNSCOPED_INFO("only the family's own file in chain"); + CHECK(names[0] == std::string("M.woff2")); } // Empty root with no children is a degenerate but valid @@ -1180,15 +1244,15 @@ namespace // and leave the registry's font map empty. Pins safe handling of // an empty skin layer (e.g., when a skin overrides only the metric // table and leaves families to inherit from a lower layer). - template<> template<> - void llfontregistry_object::test<46>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an empty root parses and resolves to no families", "[LLFontRegistry]") { const char* xml = ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); auto fams = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::ANY); - ensure_equals("empty XML produces empty available-families list", - (S32)fams.size(), 0); + UNSCOPED_INFO("empty XML produces empty available-families list"); + REQUIRE((S32)fams.size() == 0); } // applyOverrides called twice with different LLSD maps: the second @@ -1198,8 +1262,7 @@ namespace // via the per-file source-family on the prepended head file (the // override target's chain head should reflect ONLY the most recent // override's source). - template<> template<> - void llfontregistry_object::test<47>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: applyOverrides twice replaces the first override instead of merging", "[LLFontRegistry]") { const char* xml = "" @@ -1216,7 +1279,8 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); // First override: UI → AltA. UI's chain head becomes AltA's file. @@ -1224,11 +1288,12 @@ namespace ovr1["UI"] = "AltA"; applyOverrides(ovr1); const auto& files_a = templateFor("UI")->getFontFiles(); - ensure("UI chain non-empty after first override", !files_a.empty()); - ensure_equals("UI head = AltA's file", - files_a[0].FileName, std::string("A.woff2")); - ensure_equals("head's mSourceFamily = AltA", - files_a[0].mSourceFamily, std::string("AltA")); + UNSCOPED_INFO("UI chain non-empty after first override"); + REQUIRE_FALSE(files_a.empty()); + UNSCOPED_INFO("UI head = AltA's file"); + CHECK(files_a[0].FileName == std::string("A.woff2")); + UNSCOPED_INFO("head's mSourceFamily = AltA"); + CHECK(files_a[0].mSourceFamily == std::string("AltA")); // Second override: UI → AltB. The earlier UI→AltA mapping must // be replaced, not merged — UI's head is now AltB. @@ -1236,11 +1301,12 @@ namespace ovr2["UI"] = "AltB"; applyOverrides(ovr2); const auto& files_b = templateFor("UI")->getFontFiles(); - ensure("UI chain non-empty after second override", !files_b.empty()); - ensure_equals("UI head = AltB's file (replaced AltA)", - files_b[0].FileName, std::string("B.woff2")); - ensure_equals("head's mSourceFamily = AltB", - files_b[0].mSourceFamily, std::string("AltB")); + UNSCOPED_INFO("UI chain non-empty after second override"); + REQUIRE_FALSE(files_b.empty()); + UNSCOPED_INFO("UI head = AltB's file (replaced AltA)"); + CHECK(files_b[0].FileName == std::string("B.woff2")); + UNSCOPED_INFO("head's mSourceFamily = AltB"); + CHECK(files_b[0].mSourceFamily == std::string("AltB")); } // Override on a base family must propagate through chains to @@ -1256,8 +1322,7 @@ namespace // Plain -- + // Menu -- + + // Override Base → Custom must end up in BOTH Plain and Menu. - template<> template<> - void llfontregistry_object::test<48>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an override on a base family reaches every family that s it", "[LLFontRegistry]") { const char* xml = "" @@ -1286,35 +1351,36 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); LLSD overrides; overrides["Base"] = "Custom"; resolve(overrides); // Direct target picks up the override. - ensure_equals("Base[0] is the override file", - fileNames("Base").size() > 0u + UNSCOPED_INFO("Base[0] is the override file"); + REQUIRE((fileNames("Base").size() > 0u ? fileNames("Base")[0] - : std::string(), + : std::string()) == std::string("Custom.woff2")); // Both dependents see Custom.woff2 in their chain — that's the // load-bearing assertion. A regression to the old order // (overrides applied after resolution) would leave them // without Custom.woff2 entirely. - ensure("Plain chain includes Custom.woff2 via Base", - countFile("Plain", "Custom.woff2") >= 1); - ensure("Menu chain includes Custom.woff2 via Base", - countFile("Menu", "Custom.woff2") >= 1); + UNSCOPED_INFO("Plain chain includes Custom.woff2 via Base"); + CHECK(countFile("Plain", "Custom.woff2") >= 1); + UNSCOPED_INFO("Menu chain includes Custom.woff2 via Base"); + CHECK(countFile("Menu", "Custom.woff2") >= 1); // The chain after the override-driven Base files must still // carry Source (Base's own ) and the sibling s — the // override prepends, doesn't replace. - ensure("Plain chain still has Source.woff2", - countFile("Plain", "Source.woff2") >= 1); - ensure("Plain chain still has Latin.woff2", - countFile("Plain", "Latin.woff2") >= 1); - ensure("Menu chain still has Mark.woff2", - countFile("Menu", "Mark.woff2") >= 1); + UNSCOPED_INFO("Plain chain still has Source.woff2"); + CHECK(countFile("Plain", "Source.woff2") >= 1); + UNSCOPED_INFO("Plain chain still has Latin.woff2"); + CHECK(countFile("Plain", "Latin.woff2") >= 1); + UNSCOPED_INFO("Menu chain still has Mark.woff2"); + CHECK(countFile("Menu", "Mark.woff2") >= 1); // Position pin: the override file MUST be at index 0 of every // dependent chain. The renderer treats position 0 as the head @@ -1323,14 +1389,16 @@ namespace // somewhere later than 0, the original Source.woff2 still wins // for every glyph it covers and the user observes "no change." const auto plain_files = fileNames("Plain"); - ensure("Plain chain non-empty", !plain_files.empty()); - ensure_equals("Plain[0] must be the override file", - plain_files.empty() ? std::string() : plain_files[0], + UNSCOPED_INFO("Plain chain non-empty"); + REQUIRE_FALSE(plain_files.empty()); + UNSCOPED_INFO("Plain[0] must be the override file"); + REQUIRE((plain_files.empty() ? std::string() : plain_files[0]) == std::string("Custom.woff2")); const auto menu_files = fileNames("Menu"); - ensure("Menu chain non-empty", !menu_files.empty()); - ensure_equals("Menu[0] must be the override file", - menu_files.empty() ? std::string() : menu_files[0], + UNSCOPED_INFO("Menu chain non-empty"); + REQUIRE_FALSE(menu_files.empty()); + UNSCOPED_INFO("Menu[0] must be the override file"); + REQUIRE((menu_files.empty() ? std::string() : menu_files[0]) == std::string("Custom.woff2")); } @@ -1339,8 +1407,7 @@ namespace // appends the parent NORMAL chain after the variant's own files. // Verify the override (which prepends to the BOLD template) still // ends up at position 0 of dependent chains for the BOLD style. - template<> template<> - void llfontregistry_object::test<49>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an override on a base family leads the BOLD chains that it", "[LLFontRegistry]") { const char* xml = "" @@ -1363,7 +1430,8 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); LLSD overrides; overrides["Base"] = "Custom"; @@ -1371,14 +1439,15 @@ namespace // BOLD variant of Menu must lead with the override's BOLD file. const auto menu_bold = fileNames("Menu", LLFontGL::BOLD); - ensure("Menu BOLD chain non-empty", !menu_bold.empty()); - ensure_equals("Menu BOLD[0] must be override BOLD file", - menu_bold.empty() ? std::string() : menu_bold[0], + UNSCOPED_INFO("Menu BOLD chain non-empty"); + REQUIRE_FALSE(menu_bold.empty()); + UNSCOPED_INFO("Menu BOLD[0] must be override BOLD file"); + REQUIRE((menu_bold.empty() ? std::string() : menu_bold[0]) == std::string("Custom-Bold.woff2")); // NORMAL variant unchanged invariant. const auto menu_normal = fileNames("Menu", LLFontGL::NORMAL); - ensure_equals("Menu NORMAL[0] must be override NORMAL file", - menu_normal.empty() ? std::string() : menu_normal[0], + UNSCOPED_INFO("Menu NORMAL[0] must be override NORMAL file"); + REQUIRE((menu_normal.empty() ? std::string() : menu_normal[0]) == std::string("Custom-Regular.woff2")); } @@ -1389,8 +1458,7 @@ namespace // user picks one font in the picker, then picks a different one — // each setting change drives LLFontRegistry::reload(font_overrides) // which clears mFontMap before re-parsing. - template<> template<> - void llfontregistry_object::test<50>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a reload with a new override leaves no trace of the old one", "[LLFontRegistry]") { const char* xml = "" @@ -1412,55 +1480,56 @@ namespace ""; // First load: override Base -> AltA. - ensure("first parse ok", loadXml(xml)); + UNSCOPED_INFO("first parse ok"); + CHECK(loadXml(xml)); LLSD ovr_a; ovr_a["Base"] = "AltA"; resolve(ovr_a); const auto menu_after_a = fileNames("Menu"); - ensure("Menu chain non-empty after override A", - !menu_after_a.empty()); - ensure_equals("Menu[0] is AltA after first override", - menu_after_a.empty() ? std::string() : menu_after_a[0], + UNSCOPED_INFO("Menu chain non-empty after override A"); + REQUIRE_FALSE(menu_after_a.empty()); + UNSCOPED_INFO("Menu[0] is AltA after first override"); + REQUIRE((menu_after_a.empty() ? std::string() : menu_after_a[0]) == std::string("AltA.woff2")); - ensure("Menu chain has Source after first override", - countFile("Menu", "Source.woff2") >= 1); + UNSCOPED_INFO("Menu chain has Source after first override"); + CHECK(countFile("Menu", "Source.woff2") >= 1); // Simulate reload: wipe parse-time state, re-parse, apply // override B. AltA must be GONE from the chain — a regression // would leak the prior override into the new resolution. simulateReload(); - ensure("re-parse ok after simulated reload", loadXml(xml)); + UNSCOPED_INFO("re-parse ok after simulated reload"); + CHECK(loadXml(xml)); LLSD ovr_b; ovr_b["Base"] = "AltB"; resolve(ovr_b); const auto menu_after_b = fileNames("Menu"); - ensure("Menu chain non-empty after override B", - !menu_after_b.empty()); - ensure_equals("Menu[0] is AltB after second override", - menu_after_b.empty() ? std::string() : menu_after_b[0], + UNSCOPED_INFO("Menu chain non-empty after override B"); + REQUIRE_FALSE(menu_after_b.empty()); + UNSCOPED_INFO("Menu[0] is AltB after second override"); + REQUIRE((menu_after_b.empty() ? std::string() : menu_after_b[0]) == std::string("AltB.woff2")); - ensure_equals("AltA must NOT appear in Menu chain after replace", - countFile("Menu", "AltA.woff2"), 0); - ensure("Menu chain still has Source after replace", - countFile("Menu", "Source.woff2") >= 1); + UNSCOPED_INFO("AltA must NOT appear in Menu chain after replace"); + CHECK(countFile("Menu", "AltA.woff2") == 0); + UNSCOPED_INFO("Menu chain still has Source after replace"); + CHECK(countFile("Menu", "Source.woff2") >= 1); // Same checks for the direct target — Base itself must show // only the new override at chain[0], no residue from AltA. const auto base_after_b = fileNames("Base"); - ensure_equals("Base[0] is AltB after second override", - base_after_b.empty() ? std::string() : base_after_b[0], + UNSCOPED_INFO("Base[0] is AltB after second override"); + REQUIRE((base_after_b.empty() ? std::string() : base_after_b[0]) == std::string("AltB.woff2")); - ensure_equals("AltA must NOT appear in Base chain after replace", - countFile("Base", "AltA.woff2"), 0); + UNSCOPED_INFO("AltA must NOT appear in Base chain after replace"); + CHECK(countFile("Base", "AltA.woff2") == 0); } // Reload-and-clear: existing registry has override A, then user picks // "(default)" in the picker which writes an empty overrides map. // Reload with empty overrides must restore the original chain — no // override file lingers from the prior resolution. - template<> template<> - void llfontregistry_object::test<51>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a reload with no overrides restores the original chain head", "[LLFontRegistry]") { const char* xml = "" @@ -1478,27 +1547,30 @@ namespace " " ""; - ensure("first parse ok", loadXml(xml)); + UNSCOPED_INFO("first parse ok"); + CHECK(loadXml(xml)); LLSD ovr_a; ovr_a["Base"] = "AltA"; resolve(ovr_a); - ensure_equals("Menu[0] is AltA before clear", - fileNames("Menu").empty() + UNSCOPED_INFO("Menu[0] is AltA before clear"); + REQUIRE((fileNames("Menu").empty() ? std::string() - : fileNames("Menu")[0], + : fileNames("Menu")[0]) == std::string("AltA.woff2")); // Reload with empty overrides — picker's "(default)" sentinel. simulateReload(); - ensure("re-parse ok", loadXml(xml)); + UNSCOPED_INFO("re-parse ok"); + CHECK(loadXml(xml)); resolve(LLSD::emptyMap()); const auto menu = fileNames("Menu"); - ensure("Menu non-empty after clear", !menu.empty()); - ensure_equals("Menu[0] returns to Source after override clear", - menu[0], std::string("Source.woff2")); - ensure_equals("AltA gone from Menu after clear", - countFile("Menu", "AltA.woff2"), 0); + UNSCOPED_INFO("Menu non-empty after clear"); + REQUIRE_FALSE(menu.empty()); + UNSCOPED_INFO("Menu[0] returns to Source after override clear"); + CHECK(menu[0] == std::string("Source.woff2")); + UNSCOPED_INFO("AltA gone from Menu after clear"); + CHECK(countFile("Menu", "AltA.woff2") == 0); } // Dedup pin: when the override source is also reachable through the @@ -1509,8 +1581,7 @@ namespace // is functionally redundant since chain[0] already covers everything // the duplicate would, and the duplicated entry just wastes the // renderer's per-codepoint walk. - template<> template<> - void llfontregistry_object::test<52>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an override source also reached by appears once in the chain", "[LLFontRegistry]") { const char* xml = "" @@ -1528,7 +1599,8 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); LLSD overrides; overrides["Base"] = "DejaVu"; @@ -1536,14 +1608,15 @@ namespace // Without dedup, Menu would be [DejaVu(override), Source, // DejaVu(), …]. With dedup, the second DejaVu is dropped. - ensure_equals("Menu chain has DejaVu exactly once", - countFile("Menu", "DejaVu.woff2"), 1); - ensure("Menu chain still has Source", - countFile("Menu", "Source.woff2") >= 1); + UNSCOPED_INFO("Menu chain has DejaVu exactly once"); + CHECK(countFile("Menu", "DejaVu.woff2") == 1); + UNSCOPED_INFO("Menu chain still has Source"); + CHECK(countFile("Menu", "Source.woff2") >= 1); const auto menu = fileNames("Menu"); - ensure("Menu non-empty", !menu.empty()); - ensure_equals("Menu[0] is the override file", - menu[0], std::string("DejaVu.woff2")); + UNSCOPED_INFO("Menu non-empty"); + REQUIRE_FALSE(menu.empty()); + UNSCOPED_INFO("Menu[0] is the override file"); + CHECK(menu[0] == std::string("DejaVu.woff2")); } // Pure diamond, no override. A reaches D twice — once through @@ -1553,8 +1626,7 @@ namespace // also operates on plain chain composition with no override // involved, so a future regression that gates dedup on "did // applyFamilyOverrides run?" would fail here. - template<> template<> - void llfontregistry_object::test<53>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a plain diamond with no override is deduplicated too", "[LLFontRegistry]") { const char* xml = "" @@ -1572,17 +1644,18 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); // A reaches D twice (via B and via C). Pre-dedup the chain // would be [D, D]; post-dedup [D] — first hit wins, second // copy dropped. - ensure_equals("A chain has D exactly once", - countFile("A", "D.woff2"), 1); + UNSCOPED_INFO("A chain has D exactly once"); + CHECK(countFile("A", "D.woff2") == 1); const auto a_files = fileNames("A"); - ensure_equals("A chain length post-dedup", - (S32)a_files.size(), 1); + UNSCOPED_INFO("A chain length post-dedup"); + REQUIRE((S32)a_files.size() == 1); } // Two functored entries with the same filename must NOT collapse. @@ -1599,8 +1672,7 @@ namespace // `static_cast(a.CharFunctor) != static_cast(...)` // check passes (both are non-null), so the inner guard is the only // line keeping them apart. - template<> template<> - void llfontregistry_object::test<54>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: two entries of one file with different CharFunctors are both kept", "[LLFontRegistry]") { const char* xml = "" @@ -1619,23 +1691,23 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); // Both entries reference the same filename and carry a non-null // CharFunctor. Dedup must keep both — collapsing would route // every codepoint through whichever functor survived, breaking // coverage for the dropped range. - ensure_equals("Combined keeps both functored entries", - countFile("Combined", "Shared.ttf"), 2); + UNSCOPED_INFO("Combined keeps both functored entries"); + CHECK(countFile("Combined", "Shared.ttf") == 2); } // simulateReload + re-parse with new sizes must update nameToSize for // both the global table and per-family children. // Live-fonts.xml-edit reload depends on this — without it, the rest // of reload() would swap freetypes at the OLD point sizes. - template<> template<> - void llfontregistry_object::test<55>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a reload with new sizes updates nameToSize for global and family sizes", "[LLFontRegistry]") { const char* xml_v1 = "" @@ -1645,13 +1717,16 @@ namespace " " " " ""; - ensure("parse v1 ok", loadXml(xml_v1)); + UNSCOPED_INFO("parse v1 ok"); + CHECK(loadXml(xml_v1)); resolve(); F32 sz = 0.f; - ensure(reg.nameToSize("Inter", "Foo", sz)); - ensure_equals("v1: Foo (global) = 10.0", sz, 10.0f); - ensure(reg.nameToSize("Inter", "Bar", sz)); - ensure_equals("v1: Bar (per-family) = 8.0", sz, 8.0f); + CHECK(reg.nameToSize("Inter", "Foo", sz)); + UNSCOPED_INFO("v1: Foo (global) = 10.0"); + CHECK(sz == 10.0f); + CHECK(reg.nameToSize("Inter", "Bar", sz)); + UNSCOPED_INFO("v1: Bar (per-family) = 8.0"); + CHECK(sz == 8.0f); // Now simulate a runtime fonts.xml edit: wipe state, re-parse. simulateReload(); @@ -1663,12 +1738,15 @@ namespace " " " " ""; - ensure("parse v2 ok", loadXml(xml_v2)); + UNSCOPED_INFO("parse v2 ok"); + CHECK(loadXml(xml_v2)); resolve(); - ensure(reg.nameToSize("Inter", "Foo", sz)); - ensure_equals("v2: Foo (global) updated to 20.0", sz, 20.0f); - ensure(reg.nameToSize("Inter", "Bar", sz)); - ensure_equals("v2: Bar (per-family) updated to 14.5", sz, 14.5f); + CHECK(reg.nameToSize("Inter", "Foo", sz)); + UNSCOPED_INFO("v2: Foo (global) updated to 20.0"); + CHECK(sz == 20.0f); + CHECK(reg.nameToSize("Inter", "Bar", sz)); + UNSCOPED_INFO("v2: Bar (per-family) updated to 14.5"); + CHECK(sz == 14.5f); } // =================================================================== @@ -1679,8 +1757,7 @@ namespace // table. No head-face chain walk, no override-source routing. Pin // assertions stay on per-file rendering (createFont path), not on // chain-head pt resolution. - template<> template<> - void llfontregistry_object::test<56>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a use-only family's nameToSize reads the global table", "[LLFontRegistry]") { const char* xml = "" @@ -1693,15 +1770,17 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); F32 sz = 0.f; - ensure(reg.nameToSize("UseOnly", "Large", sz)); - ensure_equals("UseOnly Large = global 11.0 (no chain walk)", - sz, 11.0f); + CHECK(reg.nameToSize("UseOnly", "Large", sz)); + UNSCOPED_INFO("UseOnly Large = global 11.0 (no chain walk)"); + CHECK(sz == 11.0f); // HeadFace's own still applies for direct queries. - ensure(reg.nameToSize("HeadFace", "Large", sz)); - ensure_equals("HeadFace Large = own 12.0", sz, 12.0f); + CHECK(reg.nameToSize("HeadFace", "Large", sz)); + UNSCOPED_INFO("HeadFace Large = own 12.0"); + CHECK(sz == 12.0f); } // AlchemyUIFontOverrides changes the file list but does NOT route @@ -1710,8 +1789,7 @@ namespace // for SansSerifBase still returns its own table or global — // OpenDyslexic's applies only to OD's own files (verified // by the per-file pin tests below). - template<> template<> - void llfontregistry_object::test<57>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an override does not route the target's nameToSize to the source", "[LLFontRegistry]") { const char* xml = "" @@ -1724,27 +1802,27 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); // Baseline: SansSerifBase has no , falls to global. F32 sz = 0.f; - ensure(reg.nameToSize("SansSerifBase", "Large", sz)); - ensure_equals("baseline SansSerifBase Large = global 11.0", - sz, 11.0f); + CHECK(reg.nameToSize("SansSerifBase", "Large", sz)); + UNSCOPED_INFO("baseline SansSerifBase Large = global 11.0"); + CHECK(sz == 11.0f); // Apply override; nameToSize result must NOT change. LLSD overrides; overrides["SansSerifBase"] = "OpenDyslexic"; applyOverrides(overrides); F32 sz2 = 0.f; - ensure(reg.nameToSize("SansSerifBase", "Large", sz2)); - ensure_equals("override DOES NOT route nameToSize through OD", - sz2, 11.0f); + CHECK(reg.nameToSize("SansSerifBase", "Large", sz2)); + UNSCOPED_INFO("override DOES NOT route nameToSize through OD"); + CHECK(sz2 == 11.0f); } // Per-family on multiple distinct families: each pins its own // family's files. The data is keyed correctly per family. - template<> template<> - void llfontregistry_object::test<58>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: each family's pin is stored under that family", "[LLFontRegistry]") { const char* xml = "" @@ -1758,13 +1836,16 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); F32 a_sz = 0.f, b_sz = 0.f; - ensure(familySizePt("A", "Large", a_sz)); - ensure(familySizePt("B", "Large", b_sz)); - ensure_equals("A Large pin = 8.0", a_sz, 8.0f); - ensure_equals("B Large pin = 13.0", b_sz, 13.0f); + CHECK(familySizePt("A", "Large", a_sz)); + CHECK(familySizePt("B", "Large", b_sz)); + UNSCOPED_INFO("A Large pin = 8.0"); + CHECK(a_sz == 8.0f); + UNSCOPED_INFO("B Large pin = 13.0"); + CHECK(b_sz == 13.0f); } // Per-family from a 'd family is preserved on its files @@ -1772,8 +1853,7 @@ namespace // has ; A's resolved chain has B's file with mSourceFamily=B, // and mFamilySizes[B] still has B's pin. createFont's per-file // lookup will use B's pin for B's file, regardless of A's chain pt. - template<> template<> - void llfontregistry_object::test<59>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a d family's files keep their source family and its size pin", "[LLFontRegistry]") { const char* xml = "" @@ -1787,18 +1867,21 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); const auto& a_files = templateFor("A")->getFontFiles(); - ensure_equals("A chain has 2 files", (S32)a_files.size(), 2); - ensure_equals("A's own file tagged 'A'", - a_files[0].mSourceFamily, std::string("A")); - ensure_equals("B's file tagged 'B' through ", - a_files[1].mSourceFamily, std::string("B")); + UNSCOPED_INFO("A chain has 2 files"); + REQUIRE((S32)a_files.size() == 2); + UNSCOPED_INFO("A's own file tagged 'A'"); + CHECK(a_files[0].mSourceFamily == std::string("A")); + UNSCOPED_INFO("B's file tagged 'B' through "); + CHECK(a_files[1].mSourceFamily == std::string("B")); F32 b_sz = 0.f; - ensure("B's pin still in mFamilySizes", - familySizePt("B", "Large", b_sz)); - ensure_equals("B's pin = 8.0", b_sz, 8.0f); + UNSCOPED_INFO("B's pin still in mFamilySizes"); + CHECK(familySizePt("B", "Large", b_sz)); + UNSCOPED_INFO("B's pin = 8.0"); + CHECK(b_sz == 8.0f); } // Hot-reload of a per-family pin: edit the pin on a fallback @@ -1806,8 +1889,7 @@ namespace // in mFamilySizes. createFont's per-file lookup will then pick up // the new pin on the next load. Pins the runtime-edit path that // matters for fonts.xml live editing. - template<> template<> - void llfontregistry_object::test<62>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a reload picks up an edited per-family pin", "[LLFontRegistry]") { const char* xml_v1 = "" @@ -1817,11 +1899,13 @@ namespace " " " " ""; - ensure("parse v1 ok", loadXml(xml_v1)); + UNSCOPED_INFO("parse v1 ok"); + CHECK(loadXml(xml_v1)); resolve(); F32 v1 = 0.f; - ensure(familySizePt("B", "Large", v1)); - ensure_equals("v1: B Large pin = 8.0", v1, 8.0f); + CHECK(familySizePt("B", "Large", v1)); + UNSCOPED_INFO("v1: B Large pin = 8.0"); + CHECK(v1 == 8.0f); // Edit and reload — the new pin should land in mFamilySizes. simulateReload(); @@ -1833,11 +1917,13 @@ namespace " " " " ""; - ensure("parse v2 ok", loadXml(xml_v2)); + UNSCOPED_INFO("parse v2 ok"); + CHECK(loadXml(xml_v2)); resolve(); F32 v2 = 0.f; - ensure(familySizePt("B", "Large", v2)); - ensure_equals("v2: B Large pin = 13.5 (hot-reloaded)", v2, 13.5f); + CHECK(familySizePt("B", "Large", v2)); + UNSCOPED_INFO("v2: B Large pin = 13.5 (hot-reloaded)"); + CHECK(v2 == 13.5f); } // =================================================================== @@ -1847,8 +1933,7 @@ namespace // without a `name` attribute (new format): processNewFormatFont // logs a warning and bails before registering anything. No template // entry is created; sibling well-formed fonts still parse cleanly. - template<> template<> - void llfontregistry_object::test<64>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a without a name is dropped and its siblings still parse", "[LLFontRegistry]") { const char* xml = "" @@ -1859,18 +1944,18 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); // Empty-name template not created. The well-formed sibling is. - ensure("nameless not registered", - templateFor("", LLFontGL::NORMAL) == nullptr); - ensure("well-formed sibling parsed", - templateFor("Good", LLFontGL::NORMAL) != nullptr); + UNSCOPED_INFO("nameless not registered"); + REQUIRE(templateFor("", LLFontGL::NORMAL) == nullptr); + UNSCOPED_INFO("well-formed sibling parsed"); + REQUIRE(templateFor("Good", LLFontGL::NORMAL) != nullptr); } // missing either attribute is silently dropped (parser // checks both before insert). The well-formed entry parses normally. - template<> template<> - void llfontregistry_object::test<65>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a missing its name or size is dropped", "[LLFontRegistry]") { const char* xml = "" @@ -1878,17 +1963,20 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); F32 sz = 0.f; - ensure("OnlyName not added", !reg.nameToSize("OnlyName", sz)); - ensure("Both registered", reg.nameToSize("Both", sz)); - ensure_equals("Both = 13.0", sz, 13.0f); + UNSCOPED_INFO("OnlyName not added"); + CHECK_FALSE(reg.nameToSize("OnlyName", sz)); + UNSCOPED_INFO("Both registered"); + CHECK(reg.nameToSize("Both", sz)); + UNSCOPED_INFO("Both = 13.0"); + CHECK(sz == 13.0f); } // without a `family` attribute logs a warning and is skipped. // The chain reflects only well-formed entries. - template<> template<> - void llfontregistry_object::test<66>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a without a family is skipped and the rest still resolve", "[LLFontRegistry]") { const char* xml = "" @@ -1901,19 +1989,21 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); auto names = fileNames("A"); - ensure_equals("A own + Target via well-formed ", - (S32)names.size(), 2); - ensure_equals("A own first", names[0], std::string("A.woff2")); - ensure_equals("Target second", names[1], std::string("Target.woff2")); + UNSCOPED_INFO("A own + Target via well-formed "); + REQUIRE((S32)names.size() == 2); + UNSCOPED_INFO("A own first"); + CHECK(names[0] == std::string("A.woff2")); + UNSCOPED_INFO("Target second"); + CHECK(names[1] == std::string("Target.woff2")); } // missing name or size is dropped from the per-family table // with a warning. Sibling well-formed still applies. - template<> template<> - void llfontregistry_object::test<67>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a missing its name or size is dropped from the family table", "[LLFontRegistry]") { const char* xml = "" @@ -1925,14 +2015,15 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); F32 sz = 0.f; - ensure(reg.nameToSize("F", "Foo", sz)); - ensure_equals("F Foo = own 8.0 (well-formed entry takes effect)", - sz, 8.0f); - ensure("Malformed name 'OnlyName' did not register on F", - !reg.nameToSize("F", "OnlyName", sz)); + CHECK(reg.nameToSize("F", "Foo", sz)); + UNSCOPED_INFO("F Foo = own 8.0 (well-formed entry takes effect)"); + CHECK(sz == 8.0f); + UNSCOPED_INFO("Malformed name 'OnlyName' did not register on F"); + CHECK_FALSE(reg.nameToSize("F", "OnlyName", sz)); } // " " " ""; - ensure("parse ok", loadXml(xml)); - ensure("'WEIRD' style mapped to NORMAL", - templateFor("Quirky", LLFontGL::NORMAL) != nullptr); - ensure("no BOLD template synthesized for unrecognized style", - templateFor("Quirky", LLFontGL::BOLD) == nullptr); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); + UNSCOPED_INFO("'WEIRD' style mapped to NORMAL"); + REQUIRE(templateFor("Quirky", LLFontGL::NORMAL) != nullptr); + UNSCOPED_INFO("no BOLD template synthesized for unrecognized style"); + REQUIRE(templateFor("Quirky", LLFontGL::BOLD) == nullptr); } // Three-level cycle (A -> B -> C -> A). Visited set must // terminate the walk; each family contributes its own files exactly // once on every other family's chain. - template<> template<> - void llfontregistry_object::test<70>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a three-family cycle puts each file on the chain once", "[LLFontRegistry]") { const char* xml = "" @@ -2021,20 +2112,24 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); // Each chain should contain exactly one of each file, in the // walk order starting from the queried family. - ensure_equals("A: A.woff2 once", countFile("A", "A.woff2"), 1); - ensure_equals("A: B.woff2 once", countFile("A", "B.woff2"), 1); - ensure_equals("A: C.woff2 once", countFile("A", "C.woff2"), 1); - ensure_equals("A chain length 3", (S32)fileNames("A").size(), 3); + UNSCOPED_INFO("A: A.woff2 once"); + CHECK(countFile("A", "A.woff2") == 1); + UNSCOPED_INFO("A: B.woff2 once"); + CHECK(countFile("A", "B.woff2") == 1); + UNSCOPED_INFO("A: C.woff2 once"); + CHECK(countFile("A", "C.woff2") == 1); + UNSCOPED_INFO("A chain length 3"); + REQUIRE((S32)fileNames("A").size() == 3); } // Deep linear chain (5 levels). Walker descends without depth // restriction; final chain has each family's file exactly once. - template<> template<> - void llfontregistry_object::test<71>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a five-level chain collects every file in order", "[LLFontRegistry]") { const char* xml = "" @@ -2058,22 +2153,28 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); auto names = fileNames("F1"); - ensure_equals("F1 chain has all 5 files", (S32)names.size(), 5); - ensure_equals("F1 own first", names[0], std::string("F1.woff2")); - ensure_equals("F2 next", names[1], std::string("F2.woff2")); - ensure_equals("F3 next", names[2], std::string("F3.woff2")); - ensure_equals("F4 next", names[3], std::string("F4.woff2")); - ensure_equals("F5 last", names[4], std::string("F5.woff2")); + UNSCOPED_INFO("F1 chain has all 5 files"); + REQUIRE((S32)names.size() == 5); + UNSCOPED_INFO("F1 own first"); + CHECK(names[0] == std::string("F1.woff2")); + UNSCOPED_INFO("F2 next"); + CHECK(names[1] == std::string("F2.woff2")); + UNSCOPED_INFO("F3 next"); + CHECK(names[2] == std::string("F3.woff2")); + UNSCOPED_INFO("F4 next"); + CHECK(names[3] == std::string("F4.woff2")); + UNSCOPED_INFO("F5 last"); + CHECK(names[4] == std::string("F5.woff2")); } // inherit="true" on a non-NORMAL style with no NORMAL parent declared // logs a warning and is a no-op for that variant. The variant's chain // contains only its own files, not anything synthesized. - template<> template<> - void llfontregistry_object::test<72>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: inherit=true with no NORMAL style leaves the variant's own files", "[LLFontRegistry]") { const char* xml = "" @@ -2083,13 +2184,14 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); auto bold = fileNames("Lonely", LLFontGL::BOLD); - ensure_equals("BOLD has only its own file (no NORMAL to inherit)", - (S32)bold.size(), 1); - ensure_equals("Lonely-Bold present", - bold[0], std::string("Lonely-Bold.woff2")); + UNSCOPED_INFO("BOLD has only its own file (no NORMAL to inherit)"); + REQUIRE((S32)bold.size() == 1); + UNSCOPED_INFO("Lonely-Bold present"); + CHECK(bold[0] == std::string("Lonely-Bold.woff2")); } // Synthesis fires only for styles explicitly authored somewhere in @@ -2099,8 +2201,7 @@ namespace // (which accepts NORMAL as the closest match for any style request), // so request-time fallback handles the missing-style case rather // than synthesis aliasing every style to NORMAL. - template<> template<> - void llfontregistry_object::test<73>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a use-only family gets no template for styles its chain lacks", "[LLFontRegistry]") { const char* xml = "" @@ -2115,22 +2216,58 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); - ensure("A NORMAL synthesized (NORMAL declared in chain)", - templateFor("A", LLFontGL::NORMAL) != nullptr); - ensure("A BOLD NOT synthesized (no BOLD in chain)", - templateFor("A", LLFontGL::BOLD) == nullptr); - ensure("A ITALIC NOT synthesized (no ITALIC in chain)", - templateFor("A", LLFontGL::ITALIC) == nullptr); - ensure("A BOLD|ITALIC NOT synthesized", - templateFor("A", + UNSCOPED_INFO("A NORMAL synthesized (NORMAL declared in chain)"); + REQUIRE(templateFor("A", LLFontGL::NORMAL) != nullptr); + UNSCOPED_INFO("A BOLD NOT synthesized (no BOLD in chain)"); + REQUIRE(templateFor("A", LLFontGL::BOLD) == nullptr); + UNSCOPED_INFO("A ITALIC NOT synthesized (no ITALIC in chain)"); + REQUIRE(templateFor("A", LLFontGL::ITALIC) == nullptr); + UNSCOPED_INFO("A BOLD|ITALIC NOT synthesized"); + REQUIRE(templateFor("A", static_cast(LLFontGL::BOLD | LLFontGL::ITALIC)) == nullptr); // NORMAL synthesizes from B and C: auto normal = fileNames("A", LLFontGL::NORMAL); - ensure_equals("A NORMAL pulls both fallbacks", - (S32)normal.size(), 2); + UNSCOPED_INFO("A NORMAL pulls both fallbacks"); + REQUIRE((S32)normal.size() == 2); + } + + // Override that points to a use-only family: applyFamilyOverrides + // recognizes the source as a known family (via mFontMap template + // presence) and prepends its resolved file list. Pins the case + // where the override source is itself a composite use-only family + // — the override should still propagate the source's chain head. + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an override whose source is a use-only family prepends its chain", "[LLFontRegistry]") + { + const char* xml = + "" + " " + " " + " " + " " // use-only, source of override + " " + " " + " " + " " + " " + ""; + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); + resolve(); + LLSD overrides; + overrides["Target"] = "Composite"; + applyOverrides(overrides); + auto names = fileNames("Target"); + // Override prepends Composite's resolved chain (= Inner's files). + UNSCOPED_INFO("Target chain after override is non-empty"); + REQUIRE_FALSE(names.empty()); + UNSCOPED_INFO("Inner.woff2 prepended via Composite override"); + CHECK(names[0] == std::string("Inner.woff2")); + UNSCOPED_INFO("Target's own file kept as fallback behind override"); + CHECK(countFile("Target", "Target.woff2") == 1); } // entries round-trip through the parser into mFamilySizes, @@ -2138,8 +2275,7 @@ namespace // the family name so createFont can later look up the per-family // pin at render time. Every is an absolute pin — no force // attribute needed. - template<> template<> - void llfontregistry_object::test<75>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: pins reach mFamilySizes and files carry their source family", "[LLFontRegistry]") { const char* xml = "" @@ -2149,30 +2285,33 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); // entries land in mFamilySizes. F32 large = 0.f, medium = 0.f; - ensure(familySizePt("Pinned", "Large", large)); - ensure_equals("Large = 8.0", large, 8.0f); - ensure(familySizePt("Pinned", "Medium", medium)); - ensure_equals("Medium = 7.0", medium, 7.0f); + CHECK(familySizePt("Pinned", "Large", large)); + UNSCOPED_INFO("Large = 8.0"); + CHECK(large == 8.0f); + CHECK(familySizePt("Pinned", "Medium", medium)); + UNSCOPED_INFO("Medium = 7.0"); + CHECK(medium == 7.0f); // Files contributed by Pinned's " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); const auto& files = templateFor("A")->getFontFiles(); - ensure_equals("A chain length 2", (S32)files.size(), 2); - ensure_equals("A's own file tagged 'A'", - files[0].mSourceFamily, std::string("A")); - ensure_equals("B's file tagged 'B' (preserved through )", - files[1].mSourceFamily, std::string("B")); + UNSCOPED_INFO("A chain length 2"); + REQUIRE((S32)files.size() == 2); + UNSCOPED_INFO("A's own file tagged 'A'"); + CHECK(files[0].mSourceFamily == std::string("A")); + UNSCOPED_INFO("B's file tagged 'B' (preserved through )"); + CHECK(files[1].mSourceFamily == std::string("B")); } // Override SWAP between two distinct pinned sources, mirroring the @@ -2208,8 +2349,7 @@ namespace // after the reload (parse-time data, persists). // - Target's own nameToSize is unaffected by either override — // no cross-family routing. - template<> template<> - void llfontregistry_object::test<77>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a reload swapping pinned override sources drops the first source's file", "[LLFontRegistry]") { const char* xml = "" @@ -2228,90 +2368,60 @@ namespace ""; // Boot state: override Target -> SourceA via reload-equivalent. - ensure("parse v1 ok", loadXml(xml)); + UNSCOPED_INFO("parse v1 ok"); + CHECK(loadXml(xml)); LLSD ovr_a; ovr_a["Target"] = "SourceA"; resolve(ovr_a); const auto& a_files = templateFor("Target")->getFontFiles(); - ensure("Target chain non-empty after first override", - !a_files.empty()); - ensure_equals("Target head = SourceA's file", - a_files[0].FileName, std::string("SourceA.woff2")); - ensure_equals("head's mSourceFamily = SourceA", - a_files[0].mSourceFamily, std::string("SourceA")); + UNSCOPED_INFO("Target chain non-empty after first override"); + REQUIRE_FALSE(a_files.empty()); + UNSCOPED_INFO("Target head = SourceA's file"); + CHECK(a_files[0].FileName == std::string("SourceA.woff2")); + UNSCOPED_INFO("head's mSourceFamily = SourceA"); + CHECK(a_files[0].mSourceFamily == std::string("SourceA")); // Picker swap: production calls reload() which wipes mFontMap + // re-parses + re-resolves with the new override. simulateReload // mirrors the wipe; loadXml + resolve mirrors the re-parse + // re-resolve. simulateReload(); - ensure("parse v2 ok", loadXml(xml)); + UNSCOPED_INFO("parse v2 ok"); + CHECK(loadXml(xml)); LLSD ovr_b; ovr_b["Target"] = "SourceB"; resolve(ovr_b); const auto& b_files = templateFor("Target")->getFontFiles(); - ensure("Target chain non-empty after swap", - !b_files.empty()); - ensure_equals("Target head = SourceB's file (clean swap)", - b_files[0].FileName, std::string("SourceB.woff2")); - ensure_equals("head's mSourceFamily = SourceB", - b_files[0].mSourceFamily, std::string("SourceB")); + UNSCOPED_INFO("Target chain non-empty after swap"); + REQUIRE_FALSE(b_files.empty()); + UNSCOPED_INFO("Target head = SourceB's file (clean swap)"); + CHECK(b_files[0].FileName == std::string("SourceB.woff2")); + UNSCOPED_INFO("head's mSourceFamily = SourceB"); + CHECK(b_files[0].mSourceFamily == std::string("SourceB")); // After the reload-driven swap, SourceA's file is gone — no // leftover state from the prior override. - ensure_equals("SourceA's file is not in the post-swap chain", - countFile("Target", "SourceA.woff2"), 0); + UNSCOPED_INFO("SourceA's file is not in the post-swap chain"); + CHECK(countFile("Target", "SourceA.woff2") == 0); // Both source families' pins remain queryable in mFamilySizes // post-reload. createFont uses the head's mSourceFamily to // decide its render pt: under ovr_a that hits SourceA's 10, // under ovr_b that hits SourceB's 14. F32 a_pin = 0.f, b_pin = 0.f; - ensure(familySizePt("SourceA", "Large", a_pin)); - ensure_equals("SourceA pin = 10.0", a_pin, 10.0f); - ensure(familySizePt("SourceB", "Large", b_pin)); - ensure_equals("SourceB pin = 14.0", b_pin, 14.0f); + CHECK(familySizePt("SourceA", "Large", a_pin)); + UNSCOPED_INFO("SourceA pin = 10.0"); + CHECK(a_pin == 10.0f); + CHECK(familySizePt("SourceB", "Large", b_pin)); + UNSCOPED_INFO("SourceB pin = 14.0"); + CHECK(b_pin == 14.0f); // Target's own nameToSize is unaffected by either override — // it falls through to global because Target has no own . // Pins the "no cross-family routing" rule. F32 sz = 0.f; - ensure(reg.nameToSize("Target", "Large", sz)); - ensure_equals("Target Large = global 11.0 (override does not route)", - sz, 11.0f); - } - - // Override that points to a use-only family: applyFamilyOverrides - // recognizes the source as a known family (via mFontMap template - // presence) and prepends its resolved file list. Pins the case - // where the override source is itself a composite use-only family - // — the override should still propagate the source's chain head. - template<> template<> - void llfontregistry_object::test<74>() - { - const char* xml = - "" - " " - " " - " " - " " // use-only, source of override - " " - " " - " " - " " - " " - ""; - ensure("parse ok", loadXml(xml)); - resolve(); - LLSD overrides; - overrides["Target"] = "Composite"; - applyOverrides(overrides); - auto names = fileNames("Target"); - // Override prepends Composite's resolved chain (= Inner's files). - ensure("Target chain after override is non-empty", !names.empty()); - ensure_equals("Inner.woff2 prepended via Composite override", - names[0], std::string("Inner.woff2")); - ensure("Target's own file kept as fallback behind override", - countFile("Target", "Target.woff2") == 1); + CHECK(reg.nameToSize("Target", "Large", sz)); + UNSCOPED_INFO("Target Large = global 11.0 (override does not route)"); + CHECK(sz == 11.0f); } // LLFontDescriptor equality and hash are keyed on (name, style, size) @@ -2323,8 +2433,7 @@ namespace // file lists into the key, every chain mutation in the resolver becomes // a hash-bucket walk and lookups by request desc start missing. This // test fails noisily in that scenario. - template<> template<> - void llfontregistry_object::test<78>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: descriptor identity is name/style/size and a no-file lookup hits", "[LLFontRegistry]") { font_file_info_vec_t files_a; files_a.emplace_back("A.woff2", EFontHinting::FORCE_AUTOHINT, 0, 0.f); @@ -2335,18 +2444,18 @@ namespace LLFontDescriptor a("SansSerif", "Small", 0, files_a); LLFontDescriptor b("SansSerif", "Small", 0, files_b); - ensure("same (name,style,size) compares equal regardless of files", - a == b); - ensure_equals("same (name,style,size) hashes the same regardless of files", - hash_value(a), hash_value(b)); + UNSCOPED_INFO("same (name,style,size) compares equal regardless of files"); + CHECK(a == b); + UNSCOPED_INFO("same (name,style,size) hashes the same regardless of files"); + CHECK(hash_value(a) == hash_value(b)); // Sanity: vary each component, equality breaks. - ensure("different name is not equal", - !(a == LLFontDescriptor("Other", "Small", 0, files_a))); - ensure("different style is not equal", - !(a == LLFontDescriptor("SansSerif", "Small", LLFontGL::BOLD, files_a))); - ensure("different size is not equal", - !(a == LLFontDescriptor("SansSerif", "Large", 0, files_a))); + UNSCOPED_INFO("different name is not equal"); + CHECK_FALSE(a == LLFontDescriptor("Other", "Small", 0, files_a)); + UNSCOPED_INFO("different style is not equal"); + CHECK_FALSE(a == LLFontDescriptor("SansSerif", "Small", LLFontGL::BOLD, files_a)); + UNSCOPED_INFO("different size is not equal"); + CHECK_FALSE(a == LLFontDescriptor("SansSerif", "Large", 0, files_a)); // Lookup by no-files descriptor must hit a stored with-files entry. // This is what getMatchingFontDesc relies on at runtime. @@ -2354,39 +2463,41 @@ namespace m[a] = 1; LLFontDescriptor lookup("SansSerif", "Small", 0); // no files auto it = m.find(lookup); - ensure("lookup by no-files descriptor finds a stored with-files entry", - it != m.end()); + UNSCOPED_INFO("lookup by no-files descriptor finds a stored with-files entry"); + REQUIRE(it != m.end()); } // overridesEqual: extra coverage beyond test<36>'s identical/different // value pair. Pin the LLSD-comparison semantics initClass relies on // when deciding fast-DPI vs full-reload: empty-map and undefined-LLSD // are distinct values, and map equality is order-independent. - template<> template<> - void llfontregistry_object::test<79>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: overridesEqual tells empty from undefined and ignores key order", "[LLFontRegistry]") { const char* xml = "" " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); // Resolve with empty-map; mLastFontOverrides becomes empty-map. LLSD empty_map = LLSD::emptyMap(); resolve(empty_map); - ensure("empty map equals empty map", reg.overridesEqual(empty_map)); + UNSCOPED_INFO("empty map equals empty map"); + CHECK(reg.overridesEqual(empty_map)); // Undefined LLSD is distinct from empty-map per llsd_equals. LLSD undef; - ensure("empty map differs from undefined LLSD", - !reg.overridesEqual(undef)); + UNSCOPED_INFO("empty map differs from undefined LLSD"); + CHECK_FALSE(reg.overridesEqual(undef)); // Map equality is keyed by (key,value) pairs regardless of LLSD's // internal iteration order. Pin so a re-ordered override map // (e.g. picker rebuilds the map in different order) doesn't trip // the fast-path and force a redundant full reload. simulateReload(); - ensure("re-parse ok", loadXml(xml)); + UNSCOPED_INFO("re-parse ok"); + CHECK(loadXml(xml)); LLSD ovr1; ovr1["UI"] = "Alt"; ovr1["Other"] = "Pick"; @@ -2394,8 +2505,8 @@ namespace ovr2["Other"] = "Pick"; ovr2["UI"] = "Alt"; resolve(ovr1); - ensure("LLSD map equality is order-independent", - reg.overridesEqual(ovr2)); + UNSCOPED_INFO("LLSD map equality is order-independent"); + CHECK(reg.overridesEqual(ovr2)); } // sweepGlyphCaches must not dereference NULL-valued template entries. @@ -2404,25 +2515,25 @@ namespace // sweep during early frames before any head exists. The NULL guard at // llfontregistry.cpp:1595 covers heads; this test pins the empty- // fallback-cache path too. Crash here = regression. - template<> template<> - void llfontregistry_object::test<80>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: sweepGlyphCaches survives a registry holding only templates", "[LLFontRegistry]") { const char* xml = "" " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); // No heads instantiated, no fallback-cache entries. reg.sweepGlyphCaches(); - ensure("registry survives sweep with templates only", true); + UNSCOPED_INFO("registry survives sweep with templates only"); + CHECK(true); } // OpenType variation axes plumbed through XML parse. Family-level // font_italic / font_width / font_slant cascade to child // entries that don't override; per-file overrides win when present. - template<> template<> - void llfontregistry_object::test<81>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: font_italic/font_width/font_slant cascade to files unless overridden", "[LLFontRegistry]") { const char* xml = "" @@ -2435,44 +2546,51 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); const LLFontDescriptor* normal = templateFor("Demo", LLFontGL::NORMAL); - ensure("NORMAL template present", normal != nullptr); + UNSCOPED_INFO("NORMAL template present"); + REQUIRE(normal != nullptr); const auto& nfiles = normal->getFontFiles(); - ensure_equals("NORMAL has one file", nfiles.size(), 1u); - ensure("NORMAL inherits family ital", - nfiles[0].mVarAxes.ital_set); - ensure_equals("NORMAL ital == 1.0", nfiles[0].mVarAxes.ital, 1.0f); - ensure("NORMAL inherits family wdth", - nfiles[0].mVarAxes.wdth_set); - ensure_equals("NORMAL wdth == 87.5", nfiles[0].mVarAxes.wdth, 87.5f); - ensure("NORMAL inherits family slnt", - nfiles[0].mVarAxes.slnt_set); - ensure_equals("NORMAL slnt == -12.0", nfiles[0].mVarAxes.slnt, -12.0f); + UNSCOPED_INFO("NORMAL has one file"); + REQUIRE(nfiles.size() == 1u); + UNSCOPED_INFO("NORMAL inherits family ital"); + CHECK(nfiles[0].mVarAxes.ital_set); + UNSCOPED_INFO("NORMAL ital == 1.0"); + CHECK(nfiles[0].mVarAxes.ital == 1.0f); + UNSCOPED_INFO("NORMAL inherits family wdth"); + CHECK(nfiles[0].mVarAxes.wdth_set); + UNSCOPED_INFO("NORMAL wdth == 87.5"); + CHECK(nfiles[0].mVarAxes.wdth == 87.5f); + UNSCOPED_INFO("NORMAL inherits family slnt"); + CHECK(nfiles[0].mVarAxes.slnt_set); + UNSCOPED_INFO("NORMAL slnt == -12.0"); + CHECK(nfiles[0].mVarAxes.slnt == -12.0f); // Per-file override on wdth wins; ital/slnt still inherit. const LLFontDescriptor* bold = templateFor("Demo", LLFontGL::BOLD); - ensure("BOLD template present", bold != nullptr); + UNSCOPED_INFO("BOLD template present"); + REQUIRE(bold != nullptr); const auto& wfiles = bold->getFontFiles(); - ensure_equals("BOLD has one file", wfiles.size(), 1u); - ensure_equals("BOLD overrides wdth to 125.0", - wfiles[0].mVarAxes.wdth, 125.0f); - ensure("BOLD keeps inherited ital", - wfiles[0].mVarAxes.ital_set); - ensure_equals("BOLD inherited ital still 1.0", - wfiles[0].mVarAxes.ital, 1.0f); - ensure("BOLD keeps inherited slnt", - wfiles[0].mVarAxes.slnt_set); - ensure_equals("BOLD inherited slnt still -12.0", - wfiles[0].mVarAxes.slnt, -12.0f); + UNSCOPED_INFO("BOLD has one file"); + REQUIRE(wfiles.size() == 1u); + UNSCOPED_INFO("BOLD overrides wdth to 125.0"); + CHECK(wfiles[0].mVarAxes.wdth == 125.0f); + UNSCOPED_INFO("BOLD keeps inherited ital"); + CHECK(wfiles[0].mVarAxes.ital_set); + UNSCOPED_INFO("BOLD inherited ital still 1.0"); + CHECK(wfiles[0].mVarAxes.ital == 1.0f); + UNSCOPED_INFO("BOLD keeps inherited slnt"); + CHECK(wfiles[0].mVarAxes.slnt_set); + UNSCOPED_INFO("BOLD inherited slnt still -12.0"); + CHECK(wfiles[0].mVarAxes.slnt == -12.0f); } // Files without any axis attribute leave ALFontVarAxes at its // default (no *_set flags). Pins the negative path that the cache // key + face-load skip rely on. - template<> template<> - void llfontregistry_object::test<82>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a file with no axis attributes leaves every ALFontVarAxes flag unset", "[LLFontRegistry]") { const char* xml = "" @@ -2480,22 +2598,28 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); const LLFontDescriptor* d = templateFor("Plain", LLFontGL::NORMAL); - ensure("template present", d != nullptr); + UNSCOPED_INFO("template present"); + REQUIRE(d != nullptr); const auto& f = d->getFontFiles()[0]; - ensure("no wght", !f.mVarAxes.wght_set); - ensure("no opsz", !f.mVarAxes.opsz_set); - ensure("no ital", !f.mVarAxes.ital_set); - ensure("no wdth", !f.mVarAxes.wdth_set); - ensure("no slnt", !f.mVarAxes.slnt_set); + UNSCOPED_INFO("no wght"); + CHECK_FALSE(f.mVarAxes.wght_set); + UNSCOPED_INFO("no opsz"); + CHECK_FALSE(f.mVarAxes.opsz_set); + UNSCOPED_INFO("no ital"); + CHECK_FALSE(f.mVarAxes.ital_set); + UNSCOPED_INFO("no wdth"); + CHECK_FALSE(f.mVarAxes.wdth_set); + UNSCOPED_INFO("no slnt"); + CHECK_FALSE(f.mVarAxes.slnt_set); } // font_optical_size cascades family -> file the same way // font_weight does. Per-file override wins; absence at file level // inherits the family value. Pins the new opsz axis plumbing. - template<> template<> - void llfontregistry_object::test<83>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: font_optical_size cascades to files unless a file overrides it", "[LLFontRegistry]") { const char* xml = "" @@ -2508,32 +2632,36 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); const LLFontDescriptor* normal = templateFor("Demo", LLFontGL::NORMAL); - ensure("NORMAL template present", normal != nullptr); + UNSCOPED_INFO("NORMAL template present"); + REQUIRE(normal != nullptr); const auto& nfiles = normal->getFontFiles(); - ensure_equals("NORMAL has one file", nfiles.size(), 1u); - ensure("NORMAL inherits family opsz", - nfiles[0].mVarAxes.opsz_set); - ensure_equals("NORMAL opsz == 14.0", - nfiles[0].mVarAxes.opsz, 14.0f); + UNSCOPED_INFO("NORMAL has one file"); + REQUIRE(nfiles.size() == 1u); + UNSCOPED_INFO("NORMAL inherits family opsz"); + CHECK(nfiles[0].mVarAxes.opsz_set); + UNSCOPED_INFO("NORMAL opsz == 14.0"); + CHECK(nfiles[0].mVarAxes.opsz == 14.0f); const LLFontDescriptor* bold = templateFor("Demo", LLFontGL::BOLD); - ensure("BOLD template present", bold != nullptr); + UNSCOPED_INFO("BOLD template present"); + REQUIRE(bold != nullptr); const auto& bfiles = bold->getFontFiles(); - ensure_equals("BOLD has one file", bfiles.size(), 1u); - ensure("BOLD has opsz set after override", - bfiles[0].mVarAxes.opsz_set); - ensure_equals("BOLD overrides opsz to 28.0", - bfiles[0].mVarAxes.opsz, 28.0f); + UNSCOPED_INFO("BOLD has one file"); + REQUIRE(bfiles.size() == 1u); + UNSCOPED_INFO("BOLD has opsz set after override"); + CHECK(bfiles[0].mVarAxes.opsz_set); + UNSCOPED_INFO("BOLD overrides opsz to 28.0"); + CHECK(bfiles[0].mVarAxes.opsz == 28.0f); } // replace_first=true on a family→family override drops the target's // head file before prepending the source. Mirror of test<32> with // the new map shape. - template<> template<> - void llfontregistry_object::test<84>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: replace_first drops the target's head before prepending the source", "[LLFontRegistry]") { const char* xml = "" @@ -2544,7 +2672,8 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); LLSD overrides; LLSD entry = LLSD::emptyMap(); @@ -2553,15 +2682,15 @@ namespace overrides["UI"] = entry; applyOverrides(overrides); auto names = fileNames("UI"); - ensure_equals("head replaced -> only one file remains", - (S32)names.size(), 1); - ensure_equals("Alt is the head", names[0], std::string("Alt.woff2")); + UNSCOPED_INFO("head replaced -> only one file remains"); + REQUIRE((S32)names.size() == 1); + UNSCOPED_INFO("Alt is the head"); + CHECK(names[0] == std::string("Alt.woff2")); } // replace_first=true preserves orig_files[1..]. Verifies the // tail-merge contract: drop only the head, keep DejaVu/Emoji/CJK. - template<> template<> - void llfontregistry_object::test<85>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: replace_first keeps the target's fallbacks after its head", "[LLFontRegistry]") { const char* xml = "" @@ -2576,13 +2705,15 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); // Sanity: post-resolve UI's chain is [UI.woff2, DejaVu.woff2]. auto pre_names = fileNames("UI"); - ensure_equals("UI starts with 2 files", (S32)pre_names.size(), 2); - ensure_equals("UI head is UI.woff2", - pre_names[0], std::string("UI.woff2")); + UNSCOPED_INFO("UI starts with 2 files"); + REQUIRE((S32)pre_names.size() == 2); + UNSCOPED_INFO("UI head is UI.woff2"); + CHECK(pre_names[0] == std::string("UI.woff2")); LLSD overrides; LLSD entry = LLSD::emptyMap(); @@ -2592,18 +2723,18 @@ namespace applyOverrides(overrides); auto names = fileNames("UI"); - ensure_equals("[Alt, DejaVu] -> 2 files", - (S32)names.size(), 2); - ensure_equals("Alt is the head", names[0], std::string("Alt.woff2")); - ensure_equals("DejaVu fallback preserved", - names[1], std::string("DejaVu.woff2")); + UNSCOPED_INFO("[Alt, DejaVu] -> 2 files"); + REQUIRE((S32)names.size() == 2); + UNSCOPED_INFO("Alt is the head"); + CHECK(names[0] == std::string("Alt.woff2")); + UNSCOPED_INFO("DejaVu fallback preserved"); + CHECK(names[1] == std::string("DejaVu.woff2")); } // replace_first=true with family→file override: the target's head // is dropped and the synthesized override file takes its slot; // tail still preserved. - template<> template<> - void llfontregistry_object::test<86>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: replace_first with a file override puts the file in the head's slot", "[LLFontRegistry]") { const char* xml = "" @@ -2615,7 +2746,8 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); LLSD overrides; @@ -2626,19 +2758,19 @@ namespace applyOverrides(overrides); auto names = fileNames("UI"); - ensure_equals("[user.ttf, DejaVu] -> 2 files", - (S32)names.size(), 2); - ensure_equals("user.ttf head", names[0], std::string("user.ttf")); - ensure_equals("DejaVu fallback preserved", - names[1], std::string("DejaVu.woff2")); + UNSCOPED_INFO("[user.ttf, DejaVu] -> 2 files"); + REQUIRE((S32)names.size() == 2); + UNSCOPED_INFO("user.ttf head"); + CHECK(names[0] == std::string("user.ttf")); + UNSCOPED_INFO("DejaVu fallback preserved"); + CHECK(names[1] == std::string("DejaVu.woff2")); } // replace_first=false in the map shape behaves identically to the // bare string shape: the override is prepended onto the chain and // the target's head stays as a fallback. Pin the legacy semantics // on the new map shape so it can't accidentally diverge. - template<> template<> - void llfontregistry_object::test<87>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: replace_first=false prepends like the bare string shape", "[LLFontRegistry]") { const char* xml = "" @@ -2649,7 +2781,8 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); LLSD overrides; @@ -2660,17 +2793,17 @@ namespace applyOverrides(overrides); auto names = fileNames("UI"); - ensure_equals("map(replace_first=false) -> [Alt, UI]", - (S32)names.size(), 2); - ensure_equals("Alt prepended", names[0], std::string("Alt.woff2")); - ensure_equals("UI head preserved as fallback (no replace)", - names[1], std::string("UI.woff2")); + UNSCOPED_INFO("map(replace_first=false) -> [Alt, UI]"); + REQUIRE((S32)names.size() == 2); + UNSCOPED_INFO("Alt prepended"); + CHECK(names[0] == std::string("Alt.woff2")); + UNSCOPED_INFO("UI head preserved as fallback (no replace)"); + CHECK(names[1] == std::string("UI.woff2")); } // String shape continues to prepend (no replace). Regression guard // for the most common user-facing override path. - template<> template<> - void llfontregistry_object::test<88>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a bare string override still prepends without replacing", "[LLFontRegistry]") { const char* xml = "" @@ -2681,16 +2814,19 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); LLSD overrides; overrides["UI"] = "Alt"; // bare string applyOverrides(overrides); auto names = fileNames("UI"); - ensure_equals("string shape -> [Alt, UI]", (S32)names.size(), 2); - ensure_equals("Alt prepended", names[0], std::string("Alt.woff2")); - ensure_equals("UI head preserved as fallback", - names[1], std::string("UI.woff2")); + UNSCOPED_INFO("string shape -> [Alt, UI]"); + REQUIRE((S32)names.size() == 2); + UNSCOPED_INFO("Alt prepended"); + CHECK(names[0] == std::string("Alt.woff2")); + UNSCOPED_INFO("UI head preserved as fallback"); + CHECK(names[1] == std::string("UI.woff2")); } // attaches a CharFunctor to every file @@ -2698,8 +2834,7 @@ namespace // `` // ends up with EmojiBase's file gated by that range — accepts an // astral codepoint, rejects ASCII. - template<> template<> - void llfontregistry_object::test<89>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: gates the used family's files with a CharFunctor", "[LLFontRegistry]") { const char* xml = "" @@ -2711,27 +2846,31 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); const LLFontDescriptor* d = templateFor("Consumer"); - ensure("template present", d != nullptr); + UNSCOPED_INFO("template present"); + REQUIRE(d != nullptr); const auto& files = d->getFontFiles(); - ensure_equals("Consumer has 2 files", (S32)files.size(), 2); - ensure_equals("UI head", files[0].FileName, std::string("UI.woff2")); - ensure_equals("Noto from EmojiBase", files[1].FileName, std::string("Noto.ttf")); - ensure("Noto has a use-level functor", - static_cast(files[1].CharFunctor)); - ensure("functor accepts U+1F600", - files[1].CharFunctor((llwchar)0x1F600)); - ensure("functor rejects ASCII", - !files[1].CharFunctor((llwchar)0x0041)); + UNSCOPED_INFO("Consumer has 2 files"); + REQUIRE((S32)files.size() == 2); + UNSCOPED_INFO("UI head"); + CHECK(files[0].FileName == std::string("UI.woff2")); + UNSCOPED_INFO("Noto from EmojiBase"); + CHECK(files[1].FileName == std::string("Noto.ttf")); + UNSCOPED_INFO("Noto has a use-level functor"); + REQUIRE(static_cast(files[1].CharFunctor)); + UNSCOPED_INFO("functor accepts U+1F600"); + CHECK(files[1].CharFunctor((llwchar)0x1F600)); + UNSCOPED_INFO("functor rejects ASCII"); + CHECK_FALSE(files[1].CharFunctor((llwchar)0x0041)); } // Composition: file's own unicode_ranges intersects with the // use-level filter. Both gates must accept a codepoint for it to // pass. - template<> template<> - void llfontregistry_object::test<90>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a file's unicode_ranges and the filter must both accept", "[LLFontRegistry]") { const char* xml = "" @@ -2744,26 +2883,28 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); const LLFontDescriptor* d = templateFor("Consumer"); - ensure("template present", d != nullptr); + UNSCOPED_INFO("template present"); + REQUIRE(d != nullptr); const auto& files = d->getFontFiles(); - ensure_equals("Consumer has 1 file", (S32)files.size(), 1); - ensure("composed functor accepts U+1F500 (in both)", - files[0].CharFunctor((llwchar)0x1F500)); - ensure("composed functor rejects U+1F100 (in file, NOT in use)", - !files[0].CharFunctor((llwchar)0x1F100)); - ensure("composed functor rejects U+1F900 (in use, NOT in file)", - !files[0].CharFunctor((llwchar)0x1F900)); - ensure("composed functor rejects U+0041 (in neither)", - !files[0].CharFunctor((llwchar)0x0041)); + UNSCOPED_INFO("Consumer has 1 file"); + REQUIRE((S32)files.size() == 1); + UNSCOPED_INFO("composed functor accepts U+1F500 (in both)"); + CHECK(files[0].CharFunctor((llwchar)0x1F500)); + UNSCOPED_INFO("composed functor rejects U+1F100 (in file, NOT in use)"); + CHECK_FALSE(files[0].CharFunctor((llwchar)0x1F100)); + UNSCOPED_INFO("composed functor rejects U+1F900 (in use, NOT in file)"); + CHECK_FALSE(files[0].CharFunctor((llwchar)0x1F900)); + UNSCOPED_INFO("composed functor rejects U+0041 (in neither)"); + CHECK_FALSE(files[0].CharFunctor((llwchar)0x0041)); } // Two consumers share one source with different filters. Each // resolved chain has its own functor — no cross-talk. - template<> template<> - void llfontregistry_object::test<91>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: two rs of one family keep their own filters", "[LLFontRegistry]") { const char* xml = "" @@ -2777,27 +2918,33 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); const auto& bf = templateFor("Broad")->getFontFiles(); const auto& nf = templateFor("Narrow")->getFontFiles(); - ensure_equals("Broad has 1 file", (S32)bf.size(), 1); - ensure_equals("Narrow has 1 file", (S32)nf.size(), 1); + UNSCOPED_INFO("Broad has 1 file"); + REQUIRE((S32)bf.size() == 1); + UNSCOPED_INFO("Narrow has 1 file"); + REQUIRE((S32)nf.size() == 1); // Broad accepts U+1F100; Narrow doesn't. - ensure("Broad accepts U+1F100", bf[0].CharFunctor((llwchar)0x1F100)); - ensure("Narrow rejects U+1F100", !nf[0].CharFunctor((llwchar)0x1F100)); + UNSCOPED_INFO("Broad accepts U+1F100"); + CHECK(bf[0].CharFunctor((llwchar)0x1F100)); + UNSCOPED_INFO("Narrow rejects U+1F100"); + CHECK_FALSE(nf[0].CharFunctor((llwchar)0x1F100)); // Both accept U+1F600 (in their own range). - ensure("Broad accepts U+1F600", bf[0].CharFunctor((llwchar)0x1F600)); - ensure("Narrow accepts U+1F600", nf[0].CharFunctor((llwchar)0x1F600)); + UNSCOPED_INFO("Broad accepts U+1F600"); + CHECK(bf[0].CharFunctor((llwchar)0x1F600)); + UNSCOPED_INFO("Narrow accepts U+1F600"); + CHECK(nf[0].CharFunctor((llwchar)0x1F600)); } // Single override on the source family propagates to every consumer // with each consumer's filter intact. This is the load-bearing // motivation for use-level filters: one override entry, two chains // updated, each with its own glyph gate. - template<> template<> - void llfontregistry_object::test<92>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an override on a used family reaches each r with its filter", "[LLFontRegistry]") { const char* xml = "" @@ -2811,7 +2958,8 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); // Override at the SOURCE — EmojiBase gets prepended with Twemoji. // Apply BEFORE resolveFontReferences so the consumers' use chains @@ -2827,27 +2975,26 @@ namespace // Each consumer sees BOTH files (Twemoji prepended onto the // EmojiBase chain by the override; Noto preserved behind it), // each with the consumer's filter applied. - ensure_equals("Broad has 2 files (Twemoji + Noto)", - (S32)bf.size(), 2); - ensure_equals("Broad head is Twemoji", - bf[0].FileName, std::string("Twemoji.ttf")); - ensure_equals("Narrow has 2 files", - (S32)nf.size(), 2); - ensure_equals("Narrow head is Twemoji", - nf[0].FileName, std::string("Twemoji.ttf")); + UNSCOPED_INFO("Broad has 2 files (Twemoji + Noto)"); + REQUIRE((S32)bf.size() == 2); + UNSCOPED_INFO("Broad head is Twemoji"); + CHECK(bf[0].FileName == std::string("Twemoji.ttf")); + UNSCOPED_INFO("Narrow has 2 files"); + REQUIRE((S32)nf.size() == 2); + UNSCOPED_INFO("Narrow head is Twemoji"); + CHECK(nf[0].FileName == std::string("Twemoji.ttf")); // Filter still gates each consumer independently. - ensure("Broad's Twemoji accepts U+1F100", - bf[0].CharFunctor((llwchar)0x1F100)); - ensure("Narrow's Twemoji rejects U+1F100", - !nf[0].CharFunctor((llwchar)0x1F100)); + UNSCOPED_INFO("Broad's Twemoji accepts U+1F100"); + CHECK(bf[0].CharFunctor((llwchar)0x1F100)); + UNSCOPED_INFO("Narrow's Twemoji rejects U+1F100"); + CHECK_FALSE(nf[0].CharFunctor((llwchar)0x1F100)); } // Combined replace_first + EmojiBase: { value: "Twemoji.ttf", // replace_first: true } on EmojiBase fully displaces Noto rather // than prepending in front of it. Each consumer's chain ends up // with just the override file gated by the consumer's filter. - template<> template<> - void llfontregistry_object::test<93>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: replace_first on a used family displaces its file in every r", "[LLFontRegistry]") { const char* xml = "" @@ -2861,7 +3008,8 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); LLSD overrides; LLSD entry = LLSD::emptyMap(); @@ -2874,21 +3022,23 @@ namespace const auto& nf = templateFor("Narrow")->getFontFiles(); // replace_first dropped Noto from EmojiBase's chain; consumers // pull just Twemoji through their respective use-level filters. - ensure_equals("Broad has 1 file (Noto displaced)", - (S32)bf.size(), 1); - ensure_equals("Broad head is Twemoji", - bf[0].FileName, std::string("Twemoji.ttf")); - ensure_equals("Narrow has 1 file (Noto displaced)", - (S32)nf.size(), 1); - ensure_equals("Narrow head is Twemoji", - nf[0].FileName, std::string("Twemoji.ttf")); - ensure("Broad's Twemoji accepts U+1F100", - bf[0].CharFunctor((llwchar)0x1F100)); - ensure("Narrow's Twemoji rejects U+1F100", - !nf[0].CharFunctor((llwchar)0x1F100)); - ensure("Both accept U+1F600", - bf[0].CharFunctor((llwchar)0x1F600) - && nf[0].CharFunctor((llwchar)0x1F600)); + UNSCOPED_INFO("Broad has 1 file (Noto displaced)"); + REQUIRE((S32)bf.size() == 1); + UNSCOPED_INFO("Broad head is Twemoji"); + CHECK(bf[0].FileName == std::string("Twemoji.ttf")); + UNSCOPED_INFO("Narrow has 1 file (Noto displaced)"); + REQUIRE((S32)nf.size() == 1); + UNSCOPED_INFO("Narrow head is Twemoji"); + CHECK(nf[0].FileName == std::string("Twemoji.ttf")); + UNSCOPED_INFO("Broad's Twemoji accepts U+1F100"); + CHECK(bf[0].CharFunctor((llwchar)0x1F100)); + UNSCOPED_INFO("Narrow's Twemoji rejects U+1F100"); + CHECK_FALSE(nf[0].CharFunctor((llwchar)0x1F100)); + { + INFO("Both accept U+1F600"); + CHECK(bf[0].CharFunctor((llwchar)0x1F600)); + CHECK(nf[0].CharFunctor((llwchar)0x1F600)); + } } // No-cascade regression: a use-level filter applies ONLY to the @@ -2897,8 +3047,7 @@ namespace // explicit design rule (filter is local to the use site) — pin it // so a future "fix" that propagates the filter through the chain // doesn't silently change semantics for existing chains. - template<> template<> - void llfontregistry_object::test<94>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a filter does not cascade into the used family's own s", "[LLFontRegistry]") { const char* xml = "" @@ -2913,14 +3062,17 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); const LLFontDescriptor* d = templateFor("Outer"); - ensure("Outer template present", d != nullptr); + UNSCOPED_INFO("Outer template present"); + REQUIRE(d != nullptr); const auto& files = d->getFontFiles(); // Outer pulls Mid's own files (Mid.ttf) PLUS Mid's chain // (Inner.ttf). The filter applies only to Mid's direct files. - ensure_equals("Outer has 2 files", (S32)files.size(), 2); + UNSCOPED_INFO("Outer has 2 files"); + REQUIRE((S32)files.size() == 2); // Locate the entries by filename rather than positional index — // collect_chain orders direct files before use-chain files @@ -2933,104 +3085,29 @@ namespace if (f.FileName == "Mid.ttf") mid = &f; if (f.FileName == "Inner.ttf") inner = &f; } - ensure("Mid.ttf contributed", mid != nullptr); - ensure("Inner.ttf contributed", inner != nullptr); + UNSCOPED_INFO("Mid.ttf contributed"); + REQUIRE(mid != nullptr); + UNSCOPED_INFO("Inner.ttf contributed"); + REQUIRE(inner != nullptr); // Mid is filtered (use-level filter applied to its own files). - ensure("Mid has functor", static_cast(mid->CharFunctor)); - ensure("Mid functor accepts U+1F600", mid->CharFunctor((llwchar)0x1F600)); - ensure("Mid functor rejects U+0041", !mid->CharFunctor((llwchar)0x0041)); + UNSCOPED_INFO("Mid has functor"); + REQUIRE(static_cast(mid->CharFunctor)); + UNSCOPED_INFO("Mid functor accepts U+1F600"); + CHECK(mid->CharFunctor((llwchar)0x1F600)); + UNSCOPED_INFO("Mid functor rejects U+0041"); + CHECK_FALSE(mid->CharFunctor((llwchar)0x0041)); // Inner is NOT filtered — the use-level filter doesn't cascade. - ensure("Inner has no functor (no cascade)", - !static_cast(inner->CharFunctor)); - } - - // Baseline: a use-only EmojiBase that s NotoEmoji, consumed - // by Emoji with a unicode_ranges filter. Emoji's resolved chain - // contains exactly one Noto file (contributed via the deepest - // step, with no filter — the use-level range is dropped on - // recursion per the explicit design rule, see test<94>). - template<> template<> - void llfontregistry_object::test<98>() - { - const char* xml = - "" - " " - " " - " " - " " - " " - " " - " " - " " - " " - ""; - ensure("parse ok", loadXml(xml)); - resolve(); - const auto& files = templateFor("Emoji")->getFontFiles(); - ensure_equals("Emoji has exactly 1 file (baseline)", - (S32)files.size(), 1); - ensure_equals("the file is Noto.ttf", - files[0].FileName, std::string("Noto.ttf")); - ensure("Noto fallback has NO functor (recursion dropped use-level filter)", - !static_cast(files[0].CharFunctor)); - } - - // Override target already s the override source: a use-only - // EmojiBase that s NotoEmoji, with override EmojiBase=NotoEmoji - // (replace_first=true, matching the production picker write for - // EmojiBase). Mirrors the production workflow Preferences > Themes - // > Emoji Font: pick "Noto Emoji" while EmojiBase already s - // NotoEmoji underneath. - // - // Expected (no-op): Emoji's resolved chain stays at 1 file — - // identical to the baseline above — because the override source is - // what EmojiBase already pulls through . - // - // Observed pre-fix: Emoji's chain ends up with TWO copies of the - // file — one filtered (the override-prepended file gated by the - // use-level range), one unfiltered (the recursion - // drops the use-level filter). Step-4 dedup keeps both because - // their CharFunctors differ. The duplicate filtered head perturbs - // the shape-itemizer priority (priority-1 functored hit before the - // priority-3 no-functor hit) and is observable as differing emoji - // metrics through the codepoint vs shape paths. - template<> template<> - void llfontregistry_object::test<99>() - { - const char* xml = - "" - " " - " " - " " - " " - " " - " " - " " - " " - " " - ""; - ensure("parse ok", loadXml(xml)); - LLSD overrides; - LLSD entry = LLSD::emptyMap(); - entry["value"] = "NotoEmoji"; - entry["replace_first"] = true; - overrides["EmojiBase"] = entry; - resolve(overrides); - - const auto& files = templateFor("Emoji")->getFontFiles(); - ensure_equals("Emoji chain length unchanged by no-op override " - "(EmojiBase already s NotoEmoji)", - (S32)files.size(), 1); + UNSCOPED_INFO("Inner has no functor (no cascade)"); + REQUIRE_FALSE(static_cast(inner->CharFunctor)); } // emoji="true" attribute parses; getAvailableFamilies(EMOJI) returns // only emoji-flagged families. PROPORTIONAL and MONOSPACE filter // emoji families OUT so they don't surface in regular text-font // pickers (a color-emoji file shouldn't appear as a UI choice). - template<> template<> - void llfontregistry_object::test<95>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: emoji=true families list under EMOJI and drop out of the others", "[LLFontRegistry]") { const char* xml = "" @@ -3047,38 +3124,41 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); // EMOJI returns the two emoji families, sorted by label. auto em = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::EMOJI); - ensure_equals("EMOJI: 2 families", (S32)em.size(), 2); + UNSCOPED_INFO("EMOJI: 2 families"); + REQUIRE((S32)em.size() == 2); std::set em_names; for (const auto& f : em) em_names.insert(f.name); - ensure("EMOJI returns NotoEmoji", em_names.count("NotoEmoji") == 1); - ensure("EMOJI returns Twemoji", em_names.count("Twemoji") == 1); + UNSCOPED_INFO("EMOJI returns NotoEmoji"); + REQUIRE(em_names.count("NotoEmoji") == 1); + UNSCOPED_INFO("EMOJI returns Twemoji"); + REQUIRE(em_names.count("Twemoji") == 1); // PROPORTIONAL excludes both monospace AND emoji. auto prop = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::PROPORTIONAL); - ensure_equals("PROPORTIONAL: 1 family (just Sans)", - (S32)prop.size(), 1); - ensure_equals("PROPORTIONAL returns Sans", - prop[0].name, std::string("Sans")); + UNSCOPED_INFO("PROPORTIONAL: 1 family (just Sans)"); + REQUIRE((S32)prop.size() == 1); + UNSCOPED_INFO("PROPORTIONAL returns Sans"); + CHECK(prop[0].name == std::string("Sans")); // MONOSPACE excludes emoji families even if they were also // marked monospace=true (emoji wins for filter routing). auto mono = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::MONOSPACE); - ensure_equals("MONOSPACE: 1 family (just Mono)", - (S32)mono.size(), 1); - ensure_equals("MONOSPACE returns Mono", - mono[0].name, std::string("Mono")); + UNSCOPED_INFO("MONOSPACE: 1 family (just Mono)"); + REQUIRE((S32)mono.size() == 1); + UNSCOPED_INFO("MONOSPACE returns Mono"); + CHECK(mono[0].name == std::string("Mono")); } // emoji="true" + monospace="true" on the same family routes to // EMOJI only, not MONOSPACE. Locks the "emoji wins" rule from the // FamilyFilter docs. - template<> template<> - void llfontregistry_object::test<96>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a family both emoji and monospace lists under EMOJI only", "[LLFontRegistry]") { const char* xml = "" @@ -3086,22 +3166,22 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); auto em = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::EMOJI); - ensure_equals("EMOJI returns the dual-flagged family", - (S32)em.size(), 1); + UNSCOPED_INFO("EMOJI returns the dual-flagged family"); + REQUIRE((S32)em.size() == 1); auto mono = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::MONOSPACE); - ensure_equals("MONOSPACE excludes the dual-flagged family", - (S32)mono.size(), 0); + UNSCOPED_INFO("MONOSPACE excludes the dual-flagged family"); + REQUIRE((S32)mono.size() == 0); } // user_selectable="false" still hides a family from EMOJI just like // it hides from PROPORTIONAL/MONOSPACE. Pin so internal aliases // (EmojiBase, Emoji, LimitedEmoji) don't surface even if some // future change marks them emoji=true. - template<> template<> - void llfontregistry_object::test<97>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: user_selectable=false hides a family from EMOJI too", "[LLFontRegistry]") { const char* xml = "" @@ -3112,12 +3192,92 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); resolve(); auto em = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::EMOJI); - ensure_equals("EMOJI: only the user-selectable family", - (S32)em.size(), 1); - ensure_equals("EMOJI returns Public", - em[0].name, std::string("Public")); + UNSCOPED_INFO("EMOJI: only the user-selectable family"); + REQUIRE((S32)em.size() == 1); + UNSCOPED_INFO("EMOJI returns Public"); + CHECK(em[0].name == std::string("Public")); + } + + // Baseline: a use-only EmojiBase that s NotoEmoji, consumed + // by Emoji with a unicode_ranges filter. Emoji's resolved chain + // contains exactly one Noto file (contributed via the deepest + // step, with no filter — the use-level range is dropped on + // recursion per the explicit design rule, see test<94>). + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a filter on a use-only family leaves the deeper file unfiltered", "[LLFontRegistry]") + { + const char* xml = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); + resolve(); + const auto& files = templateFor("Emoji")->getFontFiles(); + UNSCOPED_INFO("Emoji has exactly 1 file (baseline)"); + REQUIRE((S32)files.size() == 1); + UNSCOPED_INFO("the file is Noto.ttf"); + CHECK(files[0].FileName == std::string("Noto.ttf")); + UNSCOPED_INFO("Noto fallback has NO functor (recursion dropped use-level filter)"); + REQUIRE_FALSE(static_cast(files[0].CharFunctor)); + } + + // Override target already s the override source: a use-only + // EmojiBase that s NotoEmoji, with override EmojiBase=NotoEmoji + // (replace_first=true, matching the production picker write for + // EmojiBase). Mirrors the production workflow Preferences > Themes + // > Emoji Font: pick "Noto Emoji" while EmojiBase already s + // NotoEmoji underneath. + // + // Expected (no-op): Emoji's resolved chain stays at 1 file — + // identical to the baseline above — because the override source is + // what EmojiBase already pulls through . + // + // Observed pre-fix: Emoji's chain ends up with TWO copies of the + // file — one filtered (the override-prepended file gated by the + // use-level range), one unfiltered (the recursion + // drops the use-level filter). Step-4 dedup keeps both because + // their CharFunctors differ. The duplicate filtered head perturbs + // the shape-itemizer priority (priority-1 functored hit before the + // priority-3 no-functor hit) and is observable as differing emoji + // metrics through the codepoint vs shape paths. + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an override to a family the target already s adds no file", "[LLFontRegistry]") + { + const char* xml = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + UNSCOPED_INFO("parse ok"); + CHECK(loadXml(xml)); + LLSD overrides; + LLSD entry = LLSD::emptyMap(); + entry["value"] = "NotoEmoji"; + entry["replace_first"] = true; + overrides["EmojiBase"] = entry; + resolve(overrides); + + const auto& files = templateFor("Emoji")->getFontFiles(); + UNSCOPED_INFO("Emoji chain length unchanged by no-op override " + "(EmojiBase already s NotoEmoji)"); + REQUIRE((S32)files.size() == 1); } } diff --git a/indra/llrender/tests/llfonttextcache_test.cpp b/indra/llrender/tests/llfonttextcache_test.cpp index a5f7540dd1..c431141f35 100644 --- a/indra/llrender/tests/llfonttextcache_test.cpp +++ b/indra/llrender/tests/llfonttextcache_test.cpp @@ -46,7 +46,7 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include @@ -73,8 +73,6 @@ namespace } } -namespace tut -{ namespace { // Shared GL context + LLFontGL bring-up. One per binary. TUT @@ -120,55 +118,51 @@ namespace } }; - typedef test_group llfonttextcache_test; - typedef llfonttextcache_test::object llfonttextcache_object; - tut::llfonttextcache_test llfonttextcache_testcase("LLFontTextCache"); -} // Default construction must not crash and must leave the buffer // in a destructible state. Catches uninitialized-member regressions // that surface as a crash in ~LLFontTextCache trying to free // a garbage list. - template<> template<> - void llfonttextcache_object::test<1>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: a default-constructed cache destroys cleanly", "[LLFontTextCache]") { LLFontTextCache vb; - ensure("default-constructed buffer reaches end-of-scope", true); + UNSCOPED_INFO("default-constructed buffer reaches end-of-scope"); + CHECK(true); // Implicit destruct at scope exit; assertion above catches any // crash during dtor by being unreached. } // reset() on a freshly-constructed buffer must be a safe no-op. - template<> template<> - void llfonttextcache_object::test<2>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: reset on a fresh cache is safe to call twice", "[LLFontTextCache]") { LLFontTextCache vb; vb.reset(); vb.reset(); // idempotent - ensure("reset on fresh buffer returns cleanly", true); + UNSCOPED_INFO("reset on fresh buffer returns cleanly"); + CHECK(true); } // Static toggles round-trip without affecting later test setup. // The assertions confirm the calls compile and link; observable // round-trip would require accessing the private statics. - template<> template<> - void llfonttextcache_object::test<3>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: enableBufferCollection and enableColorOnlyRegen toggle off and on", "[LLFontTextCache]") { LLFontTextCache::enableBufferCollection(false); LLFontTextCache::enableBufferCollection(true); LLFontTextCache::enableColorOnlyRegen(false); LLFontTextCache::enableColorOnlyRegen(true); - ensure("static toggles set/reset without crash", true); + UNSCOPED_INFO("static toggles set/reset without crash"); + CHECK(true); } // LLFontTextCache parallel: construct + reset. - template<> template<> - void llfonttextcache_object::test<4>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: a cache resets twice and outlives a buffer collection toggle", "[LLFontTextCache]") { LLFontTextCache wb; wb.reset(); wb.reset(); - ensure("LLFontTextCache construct + reset return cleanly", true); + UNSCOPED_INFO("LLFontTextCache construct + reset return cleanly"); + CHECK(true); LLFontTextCache::enableBufferCollection(false); LLFontTextCache::enableBufferCollection(true); } @@ -176,8 +170,7 @@ namespace // LLFontTextCache constructed alongside LLFontTextCache: // multiple coexisting instances don't share static state in a way // that breaks lifecycle. Pin two buffers in flight at once. - template<> template<> - void llfonttextcache_object::test<5>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: four caches in flight at once each reset cleanly", "[LLFontTextCache]") { LLFontTextCache a; LLFontTextCache b; @@ -187,7 +180,8 @@ namespace b.reset(); w1.reset(); w2.reset(); - ensure("multiple coexisting buffers reset cleanly", true); + UNSCOPED_INFO("multiple coexisting buffers reset cleanly"); + CHECK(true); } // =================================================================== @@ -198,54 +192,58 @@ namespace // First call computes; second call with identical params is a cache // hit and returns the same value. Both must equal fontp->getWidthF32. - template<> template<> - void llfonttextcache_object::test<6>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: getWidthBytes matches the font and repeats its answer", "[LLFontTextCache]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + UNSCOPED_INFO("font available"); + REQUIRE(font != nullptr); const std::string s = "Hello"; LLFontTextCache wb; const F32 first = wb.getWidthBytes(font, s, 0, 5, false); const F32 second = wb.getWidthBytes(font, s, 0, 5, false); const F32 ref = font->getWidthF32Bytes(s, 0, 5, false); - ensure("first width matches getWidthF32", first == ref); - ensure("second call same as first (cache hit)", - second == first); - ensure("width is positive", first > 0.f); + UNSCOPED_INFO("first width matches getWidthF32"); + CHECK(first == ref); + UNSCOPED_INFO("second call same as first (cache hit)"); + CHECK(second == first); + UNSCOPED_INFO("width is positive"); + CHECK(first > 0.f); } // Cache miss when geometry differs: changing max_chars between // calls forces recompute. The cache key includes max_chars at // llfonttextcache.cpp:517. - template<> template<> - void llfonttextcache_object::test<7>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: getWidthBytes recomputes for a new max_chars and repeats for the same", "[LLFontTextCache]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + UNSCOPED_INFO("font available"); + REQUIRE(font != nullptr); const std::string s = "Hello"; LLFontTextCache wb; const F32 w_5 = wb.getWidthBytes(font, s, 0, 5, false); const F32 w_3 = wb.getWidthBytes(font, s, 0, 3, false); - ensure("width(5) > width(3)", w_5 > w_3); + UNSCOPED_INFO("width(5) > width(3)"); + CHECK(w_5 > w_3); const F32 w_3_again = wb.getWidthBytes(font, s, 0, 3, false); - ensure_equals("identical max_chars hits cache", w_3_again, w_3); + UNSCOPED_INFO("identical max_chars hits cache"); + CHECK(w_3_again == w_3); } // Cache miss when LLFontGL::sScaleX changes between calls. Pins // mLastScaleX in the cache key (llfonttextcache.cpp:519). - template<> template<> - void llfonttextcache_object::test<8>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: getWidthBytes recomputes when LLFontGL::sScaleX changes", "[LLFontTextCache]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + UNSCOPED_INFO("font available"); + REQUIRE(font != nullptr); const std::string s = "Hi"; const F32 saved_scale = LLFontGL::sScaleX; @@ -258,20 +256,20 @@ namespace // Width should scale roughly with sScaleX (within FP rounding // noise). The cache key invalidation is the load-bearing pin. - ensure("scale=0.5 produced different width than scale=1.0", - std::abs(w_1 - w_half) > 0.01f); + UNSCOPED_INFO("scale=0.5 produced different width than scale=1.0"); + CHECK(std::abs(w_1 - w_half) > 0.01f); } // Cache miss when the font's cache generation advances between // calls (a fallback face's atlas mutation, for example). Pins // mLastFontCacheGen at llfonttextcache.cpp:524. - template<> template<> - void llfonttextcache_object::test<9>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: getWidthBytes agrees with the font across an atlas generation tick", "[LLFontTextCache]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + UNSCOPED_INFO("font available"); + REQUIRE(font != nullptr); const std::string s = "Hi"; LLFontTextCache wb; @@ -285,51 +283,53 @@ namespace // answer. Comparing a cache to itself passes whether it invalidated, // recomputed, or returned a stale value -- the only reading that can // fail is one where the buffer and the font disagree. - ensure_equals("width survives an atlas generation tick", - first, second); - ensure_equals("and still agrees with the font it came from", - second, font->getWidthF32Bytes(s, 0, 2, false)); + UNSCOPED_INFO("width survives an atlas generation tick"); + CHECK(first == second); + UNSCOPED_INFO("and still agrees with the font it came from"); + CHECK(second == font->getWidthF32Bytes(s, 0, 2, false)); } // reset() clears the cache — next call recomputes. Pins // llfonttextcache.cpp:480-493. - template<> template<> - void llfonttextcache_object::test<10>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: getWidthBytes after reset() still agrees with the font", "[LLFontTextCache]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + UNSCOPED_INFO("font available"); + REQUIRE(font != nullptr); const std::string s = "Hi"; LLFontTextCache wb; const F32 first = wb.getWidthBytes(font, s, 0, 2, false); wb.reset(); const F32 after = wb.getWidthBytes(font, s, 0, 2, false); - ensure_equals("width survives reset()", first, after); + UNSCOPED_INFO("width survives reset()"); + CHECK(first == after); // Again, against the font: a reset that failed to clear and a reset // that recomputed correctly both return the earlier value, and only // one of those also matches an independent measurement. - ensure_equals("and still agrees with the font it came from", - after, font->getWidthF32Bytes(s, 0, 2, false)); + UNSCOPED_INFO("and still agrees with the font it came from"); + CHECK(after == font->getWidthF32Bytes(s, 0, 2, false)); } // Multiple LLFontTextCache instances stay independent: cache // state in instance A doesn't affect instance B. - template<> template<> - void llfonttextcache_object::test<11>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: one cache's reset leaves another's width unchanged", "[LLFontTextCache]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + UNSCOPED_INFO("font available"); + REQUIRE(font != nullptr); const std::string s = "Hi"; LLFontTextCache a, b; const F32 wa = a.getWidthBytes(font, s, 0, 2, false); a.reset(); const F32 wb_val = b.getWidthBytes(font, s, 0, 2, false); - ensure_equals("instance B unaffected by A's reset", wa, wb_val); + UNSCOPED_INFO("instance B unaffected by A's reset"); + CHECK(wa == wb_val); } // The bound the cache is keyed on, over text where a byte is not a @@ -337,18 +337,18 @@ namespace // count, a character count and a glyph count are the same number, so a // cache keyed in the wrong one of them hits and misses identically on all // of them and every assertion still passes. - template<> template<> - void llfonttextcache_object::test<12>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: getWidthBytes keys its cache on bytes over multi-byte text", "[LLFontTextCache]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + UNSCOPED_INFO("font available"); + REQUIRE(font != nullptr); // "A", a three-byte character, "B": five bytes, three characters. const std::string s = "A\xE6\x97\xA5" "B"; if (font->getWidthF32Bytes(s, 0, (S32)s.size(), false) <= 0.f) - skip("font produced no width for the sample"); + SKIP("font produced no width for the sample"); // Every bound, boundary or not, has to agree with the font. A bound of // 1 and a bound of 4 are one character apart and three bytes apart; @@ -358,17 +358,18 @@ namespace for (S32 bound : { 0, 1, 4, 5 }) { const F32 cached = wb.getWidthBytes(font, s, 0, bound, false); - ensure_equals("cached width agrees with the font", - cached, font->getWidthF32Bytes(s, 0, bound, false)); + UNSCOPED_INFO("cached width agrees with the font"); + REQUIRE(cached == font->getWidthF32Bytes(s, 0, bound, false)); // And again, now that it is cached. - ensure_equals("and still agrees on the second reading", - wb.getWidthBytes(font, s, 0, bound, false), cached); + UNSCOPED_INFO("and still agrees on the second reading"); + REQUIRE(wb.getWidthBytes(font, s, 0, bound, false) == cached); } // Distinct bounds must not collapse onto one entry. const F32 w1 = wb.getWidthBytes(font, s, 0, 1, false); const F32 w4 = wb.getWidthBytes(font, s, 0, 4, false); - ensure("one character in is narrower than two", w1 < w4); + UNSCOPED_INFO("one character in is narrower than two"); + CHECK(w1 < w4); } // The guard that says a cache is being asked about the text it was named @@ -381,38 +382,39 @@ namespace // at one end or the other. Two long strings differing only in the middle // read as the same text, deliberately, and this is the whole of what that // bound gives up. - template<> template<> - void llfonttextcache_object::test<13>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: ALFontCacheKey tells recorded text apart by length and both ends", "[LLFontTextCache]") { ALFontCacheKey key; - ensure("the first text asked about is the one recorded", - key.sameTextAsRecorded("Inventory")); - ensure("and reads as the same text when asked again", - key.sameTextAsRecorded("Inventory")); + UNSCOPED_INFO("the first text asked about is the one recorded"); + REQUIRE(key.sameTextAsRecorded("Inventory")); + UNSCOPED_INFO("and reads as the same text when asked again"); + CHECK(key.sameTextAsRecorded("Inventory")); // What a password field does: one bullet per character, three bytes // apiece, over text that is one byte per character. - ensure("a substituted string is not the text behind it", - !key.sameTextAsRecorded("\xE2\x80\xA2\xE2\x80\xA2\xE2\x80\xA2")); + UNSCOPED_INFO("a substituted string is not the text behind it"); + CHECK_FALSE(key.sameTextAsRecorded("\xE2\x80\xA2\xE2\x80\xA2\xE2\x80\xA2")); // Same length, differing at the front and at the back -- the second is // what a sample taken only from the front would miss. ALFontCacheKey front; - ensure("records", front.sameTextAsRecorded("Received Items")); - ensure("a different string of the same length is caught", - !front.sameTextAsRecorded("Xeceived Items")); + UNSCOPED_INFO("records"); + REQUIRE(front.sameTextAsRecorded("Received Items")); + UNSCOPED_INFO("a different string of the same length is caught"); + CHECK_FALSE(front.sameTextAsRecorded("Xeceived Items")); ALFontCacheKey back; const std::string common(64, 'a'); - ensure("records", back.sameTextAsRecorded(common + "tail")); - ensure("a difference past the sampled front is caught", - !back.sameTextAsRecorded(common + "tai1")); + UNSCOPED_INFO("records"); + REQUIRE(back.sameTextAsRecorded(common + "tail")); + UNSCOPED_INFO("a difference past the sampled front is caught"); + CHECK_FALSE(back.sameTextAsRecorded(common + "tai1")); // Naming a new source is what clears the recorded text, so the next // string asked about becomes the one this is holding. key.forgetSource(); - ensure("a forgotten source records whatever is asked next", - key.sameTextAsRecorded("\xE2\x80\xA2\xE2\x80\xA2\xE2\x80\xA2")); + UNSCOPED_INFO("a forgotten source records whatever is asked next"); + CHECK(key.sameTextAsRecorded("\xE2\x80\xA2\xE2\x80\xA2\xE2\x80\xA2")); } // =================================================================== @@ -422,8 +424,6 @@ namespace // framebuffer. // =================================================================== -namespace -{ struct llfonttextcache_render_data { ll_test::HeadlessGL& gl = ll_test::sharedHeadlessGL(); @@ -441,21 +441,17 @@ namespace } }; - typedef test_group llfonttextcache_render_test; - typedef llfonttextcache_render_test::object llfonttextcache_render_object; - tut::llfonttextcache_render_test llfonttextcache_render_testcase("LLFontTextCacheRender"); -} // First render() populates the buffer cache and returns the // character count rendered. Pins genBuffers + drawBuffer end-to-end // through the stub UI shader. - template<> template<> - void llfonttextcache_render_object::test<1>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: renderBytes on a fresh cache returns the bytes it drew", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + UNSCOPED_INFO("font available"); + REQUIRE(font != nullptr); const std::string s = "Hello"; gl.clearFramebuffer(); @@ -466,7 +462,8 @@ namespace LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, /*max_chars=*/5); - ensure_equals("render returned 5 chars", n, 5); + UNSCOPED_INFO("render returned 5 chars"); + CHECK(n == 5); } // Color-only fast path (b317778477): same render twice with same @@ -474,13 +471,13 @@ namespace // recolorBuffers — no genBuffers, no atlas alloc. The global // generation counter must NOT advance between the first and second // calls (atlas already populated; recolor doesn't allocate). - template<> template<> - void llfonttextcache_render_object::test<2>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: a repeated identical render allocates nothing in the atlas", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + UNSCOPED_INFO("font available"); + REQUIRE(font != nullptr); const std::string s = "Hello"; LLFontTextCache vb; @@ -499,8 +496,8 @@ namespace LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, 5); const S32 gen_after = LLFontBitmapCache::getGlobalGeneration(); - ensure_equals("no atlas alloc on repeat render (gen counter stable)", - gen_after, gen_before); + UNSCOPED_INFO("no atlas alloc on repeat render (gen counter stable)"); + CHECK(gen_after == gen_before); } // Color change with same geometry: takes the recolor fast path — @@ -508,13 +505,13 @@ namespace // attribute, no genBuffers. Pins llfonttextcache.cpp:203-210. // Observable: global generation counter doesn't advance (no atlas // mutation), and the call returns the same character count. - template<> template<> - void llfonttextcache_render_object::test<3>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: a color change alone keeps the count and allocates nothing", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + UNSCOPED_INFO("font available"); + REQUIRE(font != nullptr); const std::string s = "Hello"; LLFontTextCache vb; @@ -527,10 +524,10 @@ namespace LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, 5); const S32 gen_after = LLFontBitmapCache::getGlobalGeneration(); - ensure_equals("color change preserves char count", - n_red, n_white); - ensure_equals("recolor fast path: no atlas mutation", - gen_after, gen_before); + UNSCOPED_INFO("color change preserves char count"); + CHECK(n_red == n_white); + UNSCOPED_INFO("recolor fast path: no atlas mutation"); + CHECK(gen_after == gen_before); } // Geometry change forces full regen: render at x=100, then at @@ -538,13 +535,13 @@ namespace // shouldn't advance (nothing about geometry change touches the // atlas), but the buffer cache had to rebuild. The post-render // render-target reflects the second position. - template<> template<> - void llfonttextcache_render_object::test<4>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: a render moved to another x returns the same count", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + UNSCOPED_INFO("font available"); + REQUIRE(font != nullptr); const std::string s = "AB"; LLFontTextCache vb; @@ -556,21 +553,23 @@ namespace const S32 n2 = vb.renderBytes(font, s, 0, 200.f, 100.f, LLColor4::white, LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, 2); - ensure_equals("char count stable after geometry change", n1, n2); - ensure("both calls returned positive count", n1 > 0); + UNSCOPED_INFO("char count stable after geometry change"); + CHECK(n1 == n2); + UNSCOPED_INFO("both calls returned positive count"); + CHECK(n1 > 0); } // enableColorOnlyRegen(false) disables the fast path entirely: // a same-geometry color-change call falls through to genBuffers // (full regen). The static toggle pins the flag-controlled branch // at llfonttextcache.cpp:210. Restore the flag in dtor. - template<> template<> - void llfonttextcache_render_object::test<5>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: with enableColorOnlyRegen off a color change still draws every byte", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + UNSCOPED_INFO("font available"); + REQUIRE(font != nullptr); const std::string s = "Hi"; LLFontTextCache vb; @@ -586,19 +585,20 @@ namespace LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, 2); LLFontTextCache::enableColorOnlyRegen(true); - ensure_equals("render with fast path disabled still returns 2", n, 2); + UNSCOPED_INFO("render with fast path disabled still returns 2"); + CHECK(n == 2); } // Style flip NORMAL → BOLD forces full regen — different glyph // index stream. Pins 00f3a4e93d (style invalidation in geometry- // invalid check at llfonttextcache.cpp:175). - template<> template<> - void llfonttextcache_render_object::test<6>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: a style flip from NORMAL to BOLD keeps the count", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + UNSCOPED_INFO("font available"); + REQUIRE(font != nullptr); const std::string s = "Hi"; LLFontTextCache vb; @@ -609,8 +609,10 @@ namespace const S32 n_bold = vb.renderBytes(font, s, 0, 100.f, 100.f, LLColor4::white, LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::BOLD, LLFontGL::NO_SHADOW, 2); - ensure_equals("char count stable across style flip", n_norm, n_bold); - ensure("both renders returned positive", n_norm > 0); + UNSCOPED_INFO("char count stable across style flip"); + CHECK(n_norm == n_bold); + UNSCOPED_INFO("both renders returned positive"); + CHECK(n_norm > 0); } // Truncated DROP_SHADOW_SOFT render with use_ellipses=true: the @@ -619,13 +621,13 @@ namespace // outer's on_pass_boundary callback and routed its shadow batch // into mForegroundBufferList — visible as off-color shadow tinting // in the recolor fast path. Fix renders the ellipsis with NO_SHADOW. - template<> template<> - void llfonttextcache_render_object::test<7>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: an ellipsized DROP_SHADOW_SOFT render adds no shadow quads for the dots", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + UNSCOPED_INFO("font available"); + REQUIRE(font != nullptr); ll_test::FontStateScope scope; @@ -648,8 +650,11 @@ namespace /*right_x=*/nullptr, /*use_ellipses=*/true, /*use_color=*/true); - ensure("truncation actually happened (n < full string)", - n > 0 && n < (S32)s.length()); + { + INFO("truncation actually happened (n < full string)"); + CHECK(n > 0); + REQUIRE(n < (S32)s.length()); + } const auto counts = ll_test::VertexBufferProbe::count(vb); @@ -657,25 +662,25 @@ namespace // NOT add to this list — the ellipsis render is single-pass // (NO_SHADOW) post-fix. const size_t expected_shadow = static_cast(n) * 5u; - ensure_equals("shadow list = visible chars * 5 (ellipsis adds 0)", - counts.shadow_quads, expected_shadow); + UNSCOPED_INFO("shadow list = visible chars * 5 (ellipsis adds 0)"); + CHECK(counts.shadow_quads == expected_shadow); // Foreground: 1 quad per visible char + 3 ellipsis chars. const size_t expected_fg = static_cast(n) + 3u; - ensure_equals("foreground list = visible + 3 ellipsis foreground", - counts.foreground_quads, expected_fg); + UNSCOPED_INFO("foreground list = visible + 3 ellipsis foreground"); + CHECK(counts.foreground_quads == expected_fg); } // Sanity check on chars_drawn: when use_ellipses=true triggers // truncation, render's return value reports only the visible // pre-ellipsis character count, not including the appended "...". - template<> template<> - void llfonttextcache_render_object::test<8>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: an ellipsized render counts the visible bytes and not the dots", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + UNSCOPED_INFO("font available"); + REQUIRE(font != nullptr); ll_test::FontStateScope scope; @@ -693,8 +698,11 @@ namespace nullptr, /*use_ellipses=*/true, true); - ensure("truncated count is positive but less than full string", - n_truncated > 0 && n_truncated < (S32)s.length()); + { + INFO("truncated count is positive but less than full string"); + CHECK(n_truncated > 0); + REQUIRE(n_truncated < (S32)s.length()); + } // Same render with max_pixels wide enough to fit the whole // string. No truncation, no ellipsis path; full count returned. @@ -709,8 +717,8 @@ namespace nullptr, /*use_ellipses=*/true, true); - ensure_equals("full-fit render returns the entire string length", - n_full, (S32)s.length()); + UNSCOPED_INFO("full-fit render returns the entire string length"); + REQUIRE(n_full == (S32)s.length()); } // Moving text does not rebuild it. Glyph geometry is built relative to the @@ -718,13 +726,13 @@ namespace // floater drag or a resize replays the capture under a new transform. That // was not true while the origin was added into the vertices: everything // visible in a moving container was reshaped every frame. - template<> template<> - void llfonttextcache_render_object::test<9>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: moving text does not rebuild it", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + UNSCOPED_INFO("font available"); + REQUIRE(font != nullptr); ll_test::FontStateScope scope; @@ -777,13 +785,15 @@ namespace LLFontGL::sCurOrigin = saved_origin; - ensure("the first draw builds", after_settle > before); + UNSCOPED_INFO("the first draw builds"); + CHECK(after_settle > before); // And without this it would hold for a cache that never replays at all. - ensure("an unmoved redraw eventually replays", settled); - ensure_equals("moving the text replays rather than rebuilds", - after_moves, after_settle); - ensure("a draw somewhere else still rebuilds", - after_real_change > after_moves); + UNSCOPED_INFO("an unmoved redraw eventually replays"); + CHECK(settled); + UNSCOPED_INFO("moving the text replays rather than rebuilds"); + CHECK(after_moves == after_settle); + UNSCOPED_INFO("a draw somewhere else still rebuilds"); + CHECK(after_real_change > after_moves); } // Text that shapes to no geometry settles too. A cell with nothing in it @@ -792,13 +802,13 @@ namespace // and a scroll list draws a cell per column whether or not it holds // anything. Such a piece of text reshaped itself on every frame it stayed // on screen, and the count is the only place it showed. - template<> template<> - void llfonttextcache_render_object::test<10>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: text that shapes to no geometry stops rebuilding too", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + UNSCOPED_INFO("font available"); + REQUIRE(font != nullptr); ll_test::FontStateScope scope; font->generateASCIIglyphs(); @@ -822,7 +832,8 @@ namespace const std::string nothing; LLFontTextCache blank; - ensure("an empty cell stops rebuilding", settle([&]() + UNSCOPED_INFO("an empty cell stops rebuilding"); + CHECK(settle([&]() { blank.setSource(¬hing, 0); blank.renderBytes(font, nothing, 0, 12.f, 34.f, LLColor4::white, @@ -832,7 +843,8 @@ namespace const std::string clipped = "Nothing of this fits"; LLFontTextCache narrow; - ensure("text with no room to draw stops rebuilding", settle([&]() + UNSCOPED_INFO("text with no room to draw stops rebuilding"); + CHECK(settle([&]() { narrow.setSource(&clipped, 0); narrow.renderBytes(font, clipped, 0, 12.f, 34.f, LLColor4::white, @@ -849,8 +861,8 @@ namespace LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, S32_MAX, /*max_pixels=*/2000); - ensure("room to draw rebuilds", - LLFontTextCache::regenCount() > before_visible); + UNSCOPED_INFO("room to draw rebuilds"); + CHECK(LLFontTextCache::regenCount() > before_visible); } // A width and a draw come off the same glyphs, so the state the font @@ -860,13 +872,13 @@ namespace // box, the ellipsis fit, the auto-resize. Without the other half being // dropped alongside, the draw was told nothing had moved and replayed a // capture whose glyphs had been rebuilt underneath it. - template<> template<> - void llfonttextcache_render_object::test<11>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: a resolution change a measurement saw still rebuilds the draw", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + UNSCOPED_INFO("font available"); + REQUIRE(font != nullptr); ll_test::FontStateScope scope; font->generateASCIIglyphs(); @@ -895,7 +907,8 @@ namespace draw(); settled = (LLFontTextCache::regenCount() == before_draw); } - ensure("measure then draw eventually replays", settled); + UNSCOPED_INFO("measure then draw eventually replays"); + CHECK(settled); const S32 saved_res_gen = LLFontGL::sResolutionGeneration; ++LLFontGL::sResolutionGeneration; @@ -908,7 +921,7 @@ namespace LLFontGL::sResolutionGeneration = saved_res_gen; - ensure("a draw after a measurement saw the same change rebuilds", - after_stale_draw > before_stale_draw); + UNSCOPED_INFO("a draw after a measurement saw the same change rebuilds"); + CHECK(after_stale_draw > before_stale_draw); } } diff --git a/indra/llrender/tests/llglslshader_gl_test.cpp b/indra/llrender/tests/llglslshader_gl_test.cpp index fddfbe5d20..b5681fa521 100644 --- a/indra/llrender/tests/llglslshader_gl_test.cpp +++ b/indra/llrender/tests/llglslshader_gl_test.cpp @@ -42,7 +42,7 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include #include @@ -53,10 +53,8 @@ #include #include -namespace tut +namespace { - namespace - { const char* kVertex = "#version 330\n" "void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }\n"; @@ -643,13 +641,15 @@ namespace tut // builds a program. void setUpShaders() { - ensure_equals("the test's names follow the product's", mShaders.mReservedUniforms.size(), + UNSCOPED_INFO("the test's names follow the product's"); + REQUIRE(mShaders.mReservedUniforms.size() == size_t(LLShaderMgr::END_RESERVED_UNIFORMS)); for (const char* name : kTestReservedNames) { mShaders.mReservedUniforms.push_back(name); } - ensure_equals("the test's blocks follow the product's", mShaders.mReservedStorageBlocks.size(), + UNSCOPED_INFO("the test's blocks follow the product's"); + REQUIRE(mShaders.mReservedStorageBlocks.size() == size_t(LLShaderMgr::END_RESERVED_STORAGE_BLOCKS)); for (const auto& block : kTestStorageBlocks) { @@ -658,8 +658,8 @@ namespace tut for (const auto& object : kStubObjects) { S32 level = 1; - ensure(std::string("compiled ") + object.first, - mShaders.loadShaderFile(object.first, level, object.second) != 0); + UNSCOPED_INFO(std::string("compiled ") + object.first); + REQUIRE(mShaders.loadShaderFile(object.first, level, object.second) != 0); } } @@ -668,7 +668,7 @@ namespace tut { if (!ll_test::contextHasCompute()) { - skip("the context has no compute"); + SKIP("the context has no compute"); } LLRender::sCompute = true; setUpShaders(); @@ -685,7 +685,11 @@ namespace tut const GLuint vs = ll_test::compileTestShader(GL_VERTEX_SHADER, kVertex); const GLuint fs = ll_test::compileTestShader(GL_FRAGMENT_SHADER, kFragment); - ensure("shaders compile", vs && fs); + { + INFO("shaders compile"); + REQUIRE(vs); + REQUIRE(fs); + } mShader.mProgramObject = glCreateProgram(); mShader.mName = "uniform cache (test)"; mShader.attachObject(vs); @@ -693,8 +697,10 @@ namespace tut const bool linked = mShader.mapAttributes(); glDeleteShader(vs); glDeleteShader(fs); - ensure("program links", linked); - ensure("program maps", mShader.mapUniforms()); + UNSCOPED_INFO("program links"); + REQUIRE(linked); + UNSCOPED_INFO("program maps"); + REQUIRE(mShader.mapUniforms()); mShader.bind(); } @@ -739,7 +745,8 @@ namespace tut size_t i = 0; for (const GLint value : expected) { - ensure_equals(what + ", component " + std::to_string(i), held[i], value); + UNSCOPED_INFO(what + ", component " + std::to_string(i)); + REQUIRE(held[i] == value); ++i; } } @@ -751,7 +758,8 @@ namespace tut size_t i = 0; for (const GLuint value : expected) { - ensure_equals(what + ", component " + std::to_string(i), held[i], value); + UNSCOPED_INFO(what + ", component " + std::to_string(i)); + REQUIRE(held[i] == value); ++i; } } @@ -763,8 +771,8 @@ namespace tut size_t i = 0; for (const F32 value : expected) { - ensure_equals(what + ", component " + std::to_string(i) + " bits", - std::bit_cast(held[i]), std::bit_cast(value)); + UNSCOPED_INFO(what + ", component " + std::to_string(i) + " bits"); + REQUIRE(std::bit_cast(held[i]) == std::bit_cast(value)); ++i; } } @@ -784,15 +792,10 @@ namespace tut LLPointer mUintImage; }; - typedef test_group llglslshader_t; - typedef llglslshader_t::object llglslshader_object_t; - tut::llglslshader_t tut_llglslshader("llglslshader_gl"); - } // Ints and uints arrive exactly past 2^24: each set below differs from the one before only in values that // convert to the same float, and only in some of its components. - template<> template<> - void llglslshader_object_t::test<1>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: ints and uints arrive exactly past 2^24", "[llglslshader_gl]") { setUp(); @@ -821,13 +824,13 @@ namespace tut mShader.uniform4uiv(LLShaderMgr::CAS_PARAM_0, 1, next); ensureUints("uvec4, w alone changed", "cas_param_0", { 0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800001u }); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A, then B, then A again holds A, for every kind of set: each upload replaces what its location's slot holds, // so the third set is compared with B. The pairs mix a setter's value and array forms, which share a kind. - template<> template<> - void llglslshader_object_t::test<2>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: setting A then B then A again holds A for every uniform type", "[llglslshader_gl]") { setUp(); @@ -878,18 +881,19 @@ namespace tut mShader.uniform1ui(LLShaderMgr::FRAME_ID, 7u); ensureUints("uint A again", "uFrameId", { 7u }); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A float set twice holds its value, and a new value replaces it. Raw uploads behind the cache's back show // which sets reached GL: just after a link the cache holds nothing, not a zero, so a set of 0 uploads; and a set // of the value last uploaded does not. - template<> template<> - void llglslshader_object_t::test<3>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: a float set skips GL only when it repeats the last value uploaded", "[llglslshader_gl]") { setUp(); const GLint loc = location("env_intensity"); - ensure("env_intensity is active", loc >= 0); + UNSCOPED_INFO("env_intensity is active"); + CHECK(loc >= 0); glUniform1f(loc, 5.f); mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.f); @@ -906,36 +910,37 @@ namespace tut mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.75f); ensureFloats("a new value replaces it", "env_intensity", { 0.75f }); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // LLGLSLShader::link forgets the cache: the relink resets every uniform to 0, and the value set before it, // set again, arrives. - template<> template<> - void llglslshader_object_t::test<4>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: LLGLSLShader::link resets uniforms to 0 and forgets the cache", "[llglslshader_gl]") { setUp(); const GLint before = location("color"); mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 1.f); ensureFloats("set before the relink", "color", { 0.25f, 0.5f, 0.75f, 1.f }); - ensure("program relinks", mShader.link()); + UNSCOPED_INFO("program relinks"); + CHECK(mShader.link()); if (location("color") != before) { - skip("the relink moved the uniform, so a stale slot would not be consulted"); + SKIP("the relink moved the uniform, so a stale slot would not be consulted"); } ensureFloats("the relink reset it", "color", { 0.f, 0.f, 0.f, 0.f }); mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 1.f); ensureFloats("set again after the relink", "color", { 0.25f, 0.5f, 0.75f, 1.f }); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // mapUniforms forgets the cache, and setMinimumAlpha's own: a new program, linked by GL alone and mapped, holds // the values set again after it, though they are the ones set on the program it replaced. - template<> template<> - void llglslshader_object_t::test<5>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: mapUniforms forgets the uniform cache and setMinimumAlpha's own", "[llglslshader_gl]") { setUp(); const GLint before = location("color"); @@ -945,17 +950,19 @@ namespace tut ensureFloats("minimum alpha on the old program", "minimum_alpha", { 0.5f }); const GLuint fresh = linkRaw(); - ensure("the new program links", fresh != 0); + UNSCOPED_INFO("the new program links"); + CHECK(fresh != 0); LLGLSLShader::unbind(); const GLuint old = mShader.mProgramObject; mShader.mProgramObject = fresh; forget_program_samplers(old); glDeleteProgram(old); - ensure("the new program maps", mShader.mapUniforms()); + UNSCOPED_INFO("the new program maps"); + CHECK(mShader.mapUniforms()); mShader.bind(); if (location("color") != before) { - skip("the new program put the uniform elsewhere, so a stale slot would not be consulted"); + SKIP("the new program put the uniform elsewhere, so a stale slot would not be consulted"); } ensureFloats("the new program starts at 0", "color", { 0.f, 0.f, 0.f, 0.f }); @@ -964,14 +971,14 @@ namespace tut mShader.setMinimumAlpha(0.5f); ensureFloats("minimum alpha set again", "minimum_alpha", { 0.5f }); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A bool takes the int setters and the float ones, and holds whichever set it last, the int set after a float // one included. INT_MIN and -0.0 share their bits: true as an int, false as a float, so only the kind a slot // records tells the float set from the int one before it. - template<> template<> - void llglslshader_object_t::test<6>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: a bool takes int and float setters and holds whichever set it last", "[llglslshader_gl]") { setUp(); @@ -990,12 +997,12 @@ namespace tut mShader.uniform1f(LLShaderMgr::GRAIN_ANIMATE, negative_zero); ensureInts("false by -0.0, the same bits", "uGrainAnimate", { 0 }); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // The reserved index and the name of one uniform share its slot: A by index, B by name, then A by index holds A. - template<> template<> - void llglslshader_object_t::test<7>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: the reserved index and the name of one uniform share its cache slot", "[llglslshader_gl]") { setUp(); const LLStaticHashedString light_count("light_count"); @@ -1008,13 +1015,13 @@ namespace tut mShader.uniform1i(light_count, 4); ensureInts("by name after the index", "light_count", { 4 }); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // The fast setters upload without comparing and leave the cache describing what they uploaded: a cached set of // the value before a fast one arrives. - template<> template<> - void llglslshader_object_t::test<8>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: fast setters upload unconditionally and leave the cache matching GL", "[llglslshader_gl]") { setUp(); @@ -1032,14 +1039,14 @@ namespace tut mShader.uniform4fv(LLShaderMgr::SPECULAR_COLOR, 1, a); ensureFloats("cached vec4 after the fast one", "specular_color", { 0.25f, 0.5f, 0.75f, 1.f }); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // An array set uploads every time and forgets what its location held: element 0 set alone, then the array over // it, then element 0 alone again holds element 0's value; and an array whose element 0 is unchanged still // reaches GL. - template<> template<> - void llglslshader_object_t::test<9>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: an array set uploads every time and forgets what its location held", "[llglslshader_gl]") { setUp(); @@ -1060,62 +1067,84 @@ namespace tut ensureFloats("element 1 by an array whose element 0 did not change", "light_diffuse[1]", { 0.25f, 0.25f, 0.25f, 0.25f }); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // Where the context has compute, its limits are at least what GL 4.3 promises, which is what the features size // themselves by. - template<> template<> - void llglslshader_object_t::test<10>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: compute limits are at least what GL 4.3 promises", "[llglslshader_gl]") { if (!gGLManager.mHasCompute) { - skip("the context has no compute"); + SKIP("the context has no compute"); } for (U32 i = 0; i < 3; ++i) { - ensure("work group count " + std::to_string(i), gGLManager.mMaxComputeWorkGroupCount[i] >= 65535); + UNSCOPED_INFO("work group count " + std::to_string(i)); + REQUIRE(gGLManager.mMaxComputeWorkGroupCount[i] >= 65535); } - ensure("work group size x", gGLManager.mMaxComputeWorkGroupSize[0] >= 1024); - ensure("work group size y", gGLManager.mMaxComputeWorkGroupSize[1] >= 1024); - ensure("work group size z", gGLManager.mMaxComputeWorkGroupSize[2] >= 64); - ensure("invocations", gGLManager.mMaxComputeWorkGroupInvocations >= 1024); - ensure("shared memory", gGLManager.mMaxComputeSharedMemorySize >= 32768); - ensure("image units", gGLManager.mMaxImageUnits >= 8); - ensure("compute storage blocks", gGLManager.mMaxComputeShaderStorageBlocks >= 8); - ensure("storage bindings", gGLManager.mMaxShaderStorageBufferBindings >= 8); - ensure("storage block size", gGLManager.mMaxShaderStorageBlockSize >= (S64(1) << 27)); + UNSCOPED_INFO("work group size x"); + CHECK(gGLManager.mMaxComputeWorkGroupSize[0] >= 1024); + UNSCOPED_INFO("work group size y"); + CHECK(gGLManager.mMaxComputeWorkGroupSize[1] >= 1024); + UNSCOPED_INFO("work group size z"); + CHECK(gGLManager.mMaxComputeWorkGroupSize[2] >= 64); + UNSCOPED_INFO("invocations"); + CHECK(gGLManager.mMaxComputeWorkGroupInvocations >= 1024); + UNSCOPED_INFO("shared memory"); + CHECK(gGLManager.mMaxComputeSharedMemorySize >= 32768); + UNSCOPED_INFO("image units"); + CHECK(gGLManager.mMaxImageUnits >= 8); + UNSCOPED_INFO("compute storage blocks"); + CHECK(gGLManager.mMaxComputeShaderStorageBlocks >= 8); + UNSCOPED_INFO("storage bindings"); + CHECK(gGLManager.mMaxShaderStorageBufferBindings >= 8); + UNSCOPED_INFO("storage block size"); + CHECK(gGLManager.mMaxShaderStorageBlockSize >= (S64(1) << 27)); const S32 alignment = gGLManager.mShaderStorageBufferOffsetAlignment; - ensure("storage offset alignment a power of two", alignment > 0 && (alignment & (alignment - 1)) == 0); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + { + INFO("storage offset alignment a power of two"); + CHECK(alignment > 0); + CHECK((alignment & (alignment - 1)) == 0); + } + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A compute program built through createShader links -- which GL refuses a compute object beside any other, so the // features' graphics objects stayed away -- from its compute object at #version 430, which it holds no longer // once linked; its work group size reads back; its block takes the table's point; and dispatchThreads over a count // that is not a multiple of the group size reaches each index exactly once. - template<> template<> - void llglslshader_object_t::test<11>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: createShader builds a compute program that reaches each index once", "[llglslshader_gl]") { setUpCompute(); - ensure_equals("a compute stage at GLSL 4.30", - compiledVersion(mShaders, "test/countC.glsl", GL_COMPUTE_SHADER, 0), std::string("#version 430")); - ensure("count program builds", - buildProgram(mCompute, "count (test)", { { "test/countC.glsl", GL_COMPUTE_SHADER } })); - ensure("it is compute", mCompute.isCompute()); - ensure("it holds no object once linked", attachedNames(mCompute.mProgramObject).empty()); - - ensure_equals("local size x", mCompute.mComputeLocalSize[0], 64u); - ensure_equals("local size y", mCompute.mComputeLocalSize[1], 1u); - ensure_equals("local size z", mCompute.mComputeLocalSize[2], 1u); - ensure("the table gives its block a point", mShaders.getStorageBlockBinding(TEST_COUNTS) >= 0); - ensure_equals("its block is at the table's point", mCompute.getStorageBinding(TEST_COUNTS), + UNSCOPED_INFO("a compute stage at GLSL 4.30"); + CHECK(compiledVersion(mShaders, "test/countC.glsl", GL_COMPUTE_SHADER, 0) == std::string("#version 430")); + UNSCOPED_INFO("count program builds"); + CHECK(buildProgram(mCompute, "count (test)", { { "test/countC.glsl", GL_COMPUTE_SHADER } })); + UNSCOPED_INFO("it is compute"); + CHECK(mCompute.isCompute()); + UNSCOPED_INFO("it holds no object once linked"); + REQUIRE(attachedNames(mCompute.mProgramObject).empty()); + + UNSCOPED_INFO("local size x"); + CHECK(mCompute.mComputeLocalSize[0] == 64u); + UNSCOPED_INFO("local size y"); + CHECK(mCompute.mComputeLocalSize[1] == 1u); + UNSCOPED_INFO("local size z"); + CHECK(mCompute.mComputeLocalSize[2] == 1u); + UNSCOPED_INFO("the table gives its block a point"); + CHECK(mShaders.getStorageBlockBinding(TEST_COUNTS) >= 0); + UNSCOPED_INFO("its block is at the table's point"); + CHECK(mCompute.getStorageBinding(TEST_COUNTS) == mShaders.getStorageBlockBinding(TEST_COUNTS)); // 15.6 groups' worth, in a buffer with room for 17. constexpr U32 COUNT = 1000; constexpr U32 WORDS = 17 * 64; - ensure("counts allocate", mBuffers[0].allocate(WORDS * sizeof(U32), 0, nullptr, "counts (test)")); + UNSCOPED_INFO("counts allocate"); + CHECK(mBuffers[0].allocate(WORDS * sizeof(U32), 0, nullptr, "counts (test)")); mBuffers[0].clear(); mCompute.bind(); @@ -1126,46 +1155,49 @@ namespace tut const std::vector hits = readWords(mBuffers[0], WORDS); for (U32 i = 0; i < WORDS; ++i) { - ensure_equals("index " + std::to_string(i) + (i < COUNT ? " once" : " untouched"), hits[i], - i < COUNT ? 1u : 0u); + UNSCOPED_INFO("index " + std::to_string(i) + (i < COUNT ? " once" : " untouched")); + REQUIRE(hits[i] == + (i < COUNT ? 1u : 0u)); } - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // The #version a stage compiles at: the usual one for an ordinary program's own stage, never raised, and for the // shared objects likewise; 4.30 for a stage asking for it. A program whose stage reads a storage block builds only // when its features ask for 4.30, which createShader hands its own stages, and links with shared objects at the // usual version. On a context below 4.30 a compute program, or one asking for 4.30, does not build. - template<> template<> - void llglslshader_object_t::test<12>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: stages compile at the usual GLSL version unless they ask for 4.30", "[llglslshader_gl]") { setUpShaders(); if (gGLManager.mGLSLVersionMajor < 4) { - skip("the context's GLSL is below 4.00"); + SKIP("the context's GLSL is below 4.00"); } const std::string usual = gGLManager.mGLSLVersionMinor >= 20 ? "#version 420" : "#version 400"; - ensure("ordinary program builds", - buildProgram(mDraw, "ladder (test)", + UNSCOPED_INFO("ordinary program builds"); + CHECK(buildProgram(mDraw, "ladder (test)", { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/solidF.glsl", GL_FRAGMENT_SHADER } })); - ensure_equals("an ordinary stage at the usual version", - compiledVersion(mShaders, "test/solidF.glsl", GL_FRAGMENT_SHADER, 0), usual); + UNSCOPED_INFO("an ordinary stage at the usual version"); + CHECK(compiledVersion(mShaders, "test/solidF.glsl", GL_FRAGMENT_SHADER, 0) == usual); size_t shared = 0; for (const auto& stage : mShaders.mShaderObjects) { for (const auto& object : stage) { - ensure_equals(object.first + " at the usual version", firstSourceLine(object.second), usual); + UNSCOPED_INFO(object.first + " at the usual version"); + REQUIRE(firstSourceLine(object.second) == usual); ++shared; } } - ensure_equals("the three shared objects", shared, size_t(3)); + UNSCOPED_INFO("the three shared objects"); + CHECK(shared == size_t(3)); if (ll_test::contextHasCompute()) { - ensure_equals("a stage asking for 4.30 at 4.30", - compiledVersion(mShaders, "test/solidF.glsl", GL_FRAGMENT_SHADER, 430), + UNSCOPED_INFO("a stage asking for 4.30 at 4.30"); + CHECK(compiledVersion(mShaders, "test/solidF.glsl", GL_FRAGMENT_SHADER, 430) == std::string("#version 430")); if (gGLManager.mMaxVertexShaderStorageBlocks >= 1) { @@ -1174,10 +1206,10 @@ namespace tut { "test/compactV.glsl", GL_VERTEX_SHADER }, { "test/compactF.glsl", GL_FRAGMENT_SHADER }, }; - ensure("a storage-block reader does not build at the usual version", - !buildProgram(mDraw2, "ladder reader (test)", reader)); - ensure("it builds asking for 4.30, beside the shared objects at the usual version", - buildProgram(mDraw2, "ladder reader 430 (test)", reader, 430)); + UNSCOPED_INFO("a storage-block reader does not build at the usual version"); + CHECK_FALSE(buildProgram(mDraw2, "ladder reader (test)", reader)); + UNSCOPED_INFO("it builds asking for 4.30, beside the shared objects at the usual version"); + CHECK(buildProgram(mDraw2, "ladder reader 430 (test)", reader, 430)); } } @@ -1194,39 +1226,47 @@ namespace tut 430); gGLManager.mGLSLVersionMajor = major; gGLManager.mGLSLVersionMinor = minor; - ensure("no compute program below 4.30", !compute_built); - ensure("no program asking for 4.30 below it", !raised_built); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no compute program below 4.30"); + CHECK_FALSE(compute_built); + UNSCOPED_INFO("no program asking for 4.30 below it"); + CHECK_FALSE(raised_built); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A kernel stores into an R32F and an RGBA16F target through their programs' image units. Past the barriers the // textures hold the pattern, read back directly and sampled by a fragment program. - template<> template<> - void llglslshader_object_t::test<13>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: a kernel stores into R32F and RGBA16F targets through image units", "[llglslshader_gl]") { setUpCompute(); constexpr U32 W = 37; constexpr U32 H = 23; - ensure("R32F target allocates", mTargets[0].allocate(W, H, GL_R32F)); - ensure("RGBA16F target allocates", mTargets[1].allocate(W, H, GL_RGBA16F)); - ensure("output target allocates", mTargets[2].allocate(W, H, GL_RGBA32F)); - ensure("store program builds", - buildProgram(mCompute, "image store (test)", { { "test/imageStoreC.glsl", GL_COMPUTE_SHADER } })); - ensure("sample program builds", - buildProgram(mDraw, "image sample (test)", + UNSCOPED_INFO("R32F target allocates"); + CHECK(mTargets[0].allocate(W, H, GL_R32F)); + UNSCOPED_INFO("RGBA16F target allocates"); + CHECK(mTargets[1].allocate(W, H, GL_RGBA16F)); + UNSCOPED_INFO("output target allocates"); + CHECK(mTargets[2].allocate(W, H, GL_RGBA32F)); + UNSCOPED_INFO("store program builds"); + CHECK(buildProgram(mCompute, "image store (test)", { { "test/imageStoreC.glsl", GL_COMPUTE_SHADER } })); + UNSCOPED_INFO("sample program builds"); + CHECK(buildProgram(mDraw, "image sample (test)", { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/sampleF.glsl", GL_FRAGMENT_SHADER } })); const S32 r32f_unit = mCompute.getImageUnit(TEST_IMAGE_R32F); const S32 rgba_unit = mCompute.getImageUnit(TEST_IMAGE_RGBA16F); - ensure("R32F image has a unit", r32f_unit >= 0); - ensure("RGBA16F image has a unit", rgba_unit >= 0); - ensure("the units differ", r32f_unit != rgba_unit); + UNSCOPED_INFO("R32F image has a unit"); + CHECK(r32f_unit >= 0); + UNSCOPED_INFO("RGBA16F image has a unit"); + CHECK(rgba_unit >= 0); + UNSCOPED_INFO("the units differ"); + CHECK(r32f_unit != rgba_unit); mCompute.bind(); - ensure_equals("R32F bound at its unit", - mCompute.bindImage(TEST_IMAGE_R32F, &mTargets[0], ALImageSlot::WRITE_ONLY), r32f_unit); - ensure_equals("RGBA16F bound at its unit", - mCompute.bindImage(TEST_IMAGE_RGBA16F, &mTargets[1], ALImageSlot::WRITE_ONLY), rgba_unit); + UNSCOPED_INFO("R32F bound at its unit"); + CHECK(mCompute.bindImage(TEST_IMAGE_R32F, &mTargets[0], ALImageSlot::WRITE_ONLY) == r32f_unit); + UNSCOPED_INFO("RGBA16F bound at its unit"); + CHECK(mCompute.bindImage(TEST_IMAGE_RGBA16F, &mTargets[1], ALImageSlot::WRITE_ONLY) == rgba_unit); mCompute.dispatchThreads(W, H); gGL.memoryBarrier(LLRender::BARRIER_TEXTURE_FETCH | LLRender::BARRIER_TEXTURE_UPDATE); mCompute.unbindImage(TEST_IMAGE_R32F); @@ -1240,11 +1280,16 @@ namespace tut { const std::string at = " at " + std::to_string(x) + "," + std::to_string(y); const size_t i = (size_t)y * W + x; - ensure_equals("R32F" + at, r32f[i], F32(x * 1000 + y)); - ensure_equals("RGBA16F r" + at, rgba[i * 4 + 0], F32(x) * 0.125f); - ensure_equals("RGBA16F g" + at, rgba[i * 4 + 1], F32(y) * 0.125f); - ensure_equals("RGBA16F b" + at, rgba[i * 4 + 2], 0.25f); - ensure_equals("RGBA16F a" + at, rgba[i * 4 + 3], 1.f); + UNSCOPED_INFO("R32F" + at); + REQUIRE(r32f[i] == F32(x * 1000 + y)); + UNSCOPED_INFO("RGBA16F r" + at); + REQUIRE(rgba[i * 4 + 0] == F32(x) * 0.125f); + UNSCOPED_INFO("RGBA16F g" + at); + REQUIRE(rgba[i * 4 + 1] == F32(y) * 0.125f); + UNSCOPED_INFO("RGBA16F b" + at); + REQUIRE(rgba[i * 4 + 2] == 0.25f); + UNSCOPED_INFO("RGBA16F a" + at); + REQUIRE(rgba[i * 4 + 3] == 1.f); } } @@ -1255,7 +1300,8 @@ namespace tut gGL.setClearColor(LLColor4::transparent); mTargets[2].clear(); mDraw.bind(); - ensure("source bound", mDraw.bindTexture(TEST_SOURCE_MAP, &mTargets[source], ALSamplers::PointClamp) >= 0); + UNSCOPED_INFO("source bound"); + REQUIRE(mDraw.bindTexture(TEST_SOURCE_MAP, &mTargets[source], ALSamplers::PointClamp) >= 0); gGL.drawArraysInstanced(LLRender::TRIANGLES, 0, 3, 1); const std::vector drawn = readBoundTarget(W, H, 4, GL_RGBA); mTargets[2].flush(); @@ -1277,25 +1323,26 @@ namespace tut }; for (U32 c = 0; c < 4; ++c) { - ensure_equals("component " + std::to_string(c) + at, drawn[i + c], expected[c]); + UNSCOPED_INFO("component " + std::to_string(c) + at); + REQUIRE(drawn[i + c] == expected[c]); } } } } - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // GPU compaction drawn indirectly: a kernel keeps the ids passing a test, counting them into the instance count // of the draw command at the head of their list; past the barriers an indirect draw, whose vertex stage reads the // same buffer as a storage block, puts one point at each kept id's pixel and none anywhere else. The list is bound // once, for the kernel, and the draw finds it at the same point. - template<> template<> - void llglslshader_object_t::test<14>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: a compacted id list drawn indirectly puts one point at each kept id", "[llglslshader_gl]") { setUpCompute(); if (gGLManager.mMaxVertexShaderStorageBlocks < 1) { - skip("the context's vertex stage reads no storage blocks"); + SKIP("the context's vertex stage reads no storage blocks"); } constexpr U32 COUNT = 100; constexpr U32 SIDE = 16; @@ -1304,17 +1351,21 @@ namespace tut // One vertex a point, no instances yet, then room for every id. std::vector list(4 + COUNT, 0u); list[0] = 1u; - ensure("list allocates", mBuffers[0].allocate(list.size() * sizeof(U32), 0, list.data(), "compaction (test)")); - ensure("output target allocates", mTargets[0].allocate(SIDE, SIDE, GL_R32F)); - ensure("compaction builds", - buildProgram(mCompute, "compaction (test)", { { "test/compactC.glsl", GL_COMPUTE_SHADER } })); - ensure("draw builds", - buildProgram(mDraw, "compaction draw (test)", + UNSCOPED_INFO("list allocates"); + REQUIRE(mBuffers[0].allocate(list.size() * sizeof(U32), 0, list.data(), "compaction (test)")); + UNSCOPED_INFO("output target allocates"); + CHECK(mTargets[0].allocate(SIDE, SIDE, GL_R32F)); + UNSCOPED_INFO("compaction builds"); + CHECK(buildProgram(mCompute, "compaction (test)", { { "test/compactC.glsl", GL_COMPUTE_SHADER } })); + UNSCOPED_INFO("draw builds"); + CHECK(buildProgram(mDraw, "compaction draw (test)", { { "test/compactV.glsl", GL_VERTEX_SHADER }, { "test/compactF.glsl", GL_FRAGMENT_SHADER } }, 430)); const S32 point = mShaders.getStorageBlockBinding(TEST_DRAW_LIST); - ensure_equals("the kernel's list at the table's point", mCompute.getStorageBinding(TEST_DRAW_LIST), point); - ensure_equals("and the draw's", mDraw.getStorageBinding(TEST_DRAW_LIST), point); + UNSCOPED_INFO("the kernel's list at the table's point"); + CHECK(mCompute.getStorageBinding(TEST_DRAW_LIST) == point); + UNSCOPED_INFO("and the draw's"); + CHECK(mDraw.getStorageBinding(TEST_DRAW_LIST) == point); mCompute.bind(); mCompute.uniform1i(TEST_COUNT, (GLint)COUNT); @@ -1328,7 +1379,8 @@ namespace tut mTargets[0].clear(); mDraw.bind(); mDraw.uniform1i(TEST_WIDTH, (GLint)SIDE); - ensure_equals("the list still bound for the draw", gGL.getStorageBuffer((U32)point), mBuffers[0].getGLName()); + UNSCOPED_INFO("the list still bound for the draw"); + CHECK(gGL.getStorageBuffer((U32)point) == mBuffers[0].getGLName()); gGL.drawArraysIndirect(LLRender::POINTS, mBuffers[0], 0); const std::vector drawn = readBoundTarget(SIDE, SIDE, 1, GL_RED); mTargets[0].flush(); @@ -1336,26 +1388,30 @@ namespace tut for (U32 id = 0; id < SIDE * SIDE; ++id) { const bool kept = id < COUNT && id % 3 == 0; - ensure_equals("id " + std::to_string(id) + (kept ? " drawn" : " not drawn"), drawn[id], - kept ? F32(id + 1) : 0.f); + UNSCOPED_INFO("id " + std::to_string(id) + (kept ? " drawn" : " not drawn")); + REQUIRE(drawn[id] == + (kept ? F32(id + 1) : 0.f)); } - ensure_equals("instances counted", readWords(mBuffers[0], 2)[1], KEPT); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("instances counted"); + CHECK(readWords(mBuffers[0], 2)[1] == KEPT); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // dispatchIndirect runs the group counts a buffer holds, here written by a kernel before it 16 bytes in: every // invocation of 5 x 2 x 1 groups of 4 counted once, and the last group seen in each direction. - template<> template<> - void llglslshader_object_t::test<15>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: dispatchIndirect runs the group counts a buffer holds", "[llglslshader_gl]") { setUpCompute(); const std::vector zeros(8, 0u); - ensure("command buffer allocates", mBuffers[0].allocate(8 * sizeof(U32), 0, zeros.data(), "dispatch (test)")); - ensure("counts allocate", mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "counts (test)")); - ensure("writer builds", - buildProgram(mCompute, "dispatch writer (test)", { { "test/indirectWriteC.glsl", GL_COMPUTE_SHADER } })); - ensure("counter builds", - buildProgram(mCompute2, "dispatch counter (test)", { { "test/indirectCountC.glsl", GL_COMPUTE_SHADER } })); + UNSCOPED_INFO("command buffer allocates"); + CHECK(mBuffers[0].allocate(8 * sizeof(U32), 0, zeros.data(), "dispatch (test)")); + UNSCOPED_INFO("counts allocate"); + CHECK(mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "counts (test)")); + UNSCOPED_INFO("writer builds"); + CHECK(buildProgram(mCompute, "dispatch writer (test)", { { "test/indirectWriteC.glsl", GL_COMPUTE_SHADER } })); + UNSCOPED_INFO("counter builds"); + CHECK(buildProgram(mCompute2, "dispatch counter (test)", { { "test/indirectCountC.glsl", GL_COMPUTE_SHADER } })); mCompute.bind(); mCompute.uniform1i(TEST_COUNT, 5); @@ -1368,40 +1424,56 @@ namespace tut mCompute2.dispatchIndirect(mBuffers[0], 16); const std::vector command = readWords(mBuffers[0], 7); - ensure_equals("command x", command[4], 5u); - ensure_equals("command y", command[5], 2u); - ensure_equals("command z", command[6], 1u); + UNSCOPED_INFO("command x"); + CHECK(command[4] == 5u); + UNSCOPED_INFO("command y"); + CHECK(command[5] == 2u); + UNSCOPED_INFO("command z"); + CHECK(command[6] == 1u); const std::vector hits = readWords(mBuffers[1], 4); - ensure_equals("invocations", hits[0], 5u * 2u * 1u * 4u); - ensure_equals("groups in x", hits[1], 5u); - ensure_equals("groups in y", hits[2], 2u); - ensure_equals("groups in z", hits[3], 1u); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("invocations"); + CHECK(hits[0] == 5u * 2u * 1u * 4u); + UNSCOPED_INFO("groups in x"); + CHECK(hits[1] == 5u); + UNSCOPED_INFO("groups in y"); + CHECK(hits[2] == 2u); + UNSCOPED_INFO("groups in z"); + CHECK(hits[3] == 1u); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // Two storage blocks declared in the order opposite to the table's take the table's points, which run in its // order, and bindStorageBuffer routes each buffer to its own block: swapping the buffers swaps what each holds. - template<> template<> - void llglslshader_object_t::test<16>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: storage blocks declared out of the table's order still take its points", "[llglslshader_gl]") { setUpCompute(); - ensure("remap builds", buildProgram(mCompute, "block remap (test)", { { "test/remapC.glsl", GL_COMPUTE_SHADER } })); + UNSCOPED_INFO("remap builds"); + CHECK(buildProgram(mCompute, "block remap (test)", { { "test/remapC.glsl", GL_COMPUTE_SHADER } })); const S32 first = mShaders.getStorageBlockBinding(TEST_BLOCK_FIRST); - ensure_equals("the first block at its point", mCompute.getStorageBinding(TEST_BLOCK_FIRST), first); - ensure_equals("the second at the next", mCompute.getStorageBinding(TEST_BLOCK_SECOND), first + 1); - ensure_equals("an undeclared block has none", mCompute.getStorageBinding(TEST_COUNTS), -1); - ensure_equals("nor does an index past the table", mCompute.getStorageBinding(TEST_STORAGE_END), -1); + UNSCOPED_INFO("the first block at its point"); + CHECK(mCompute.getStorageBinding(TEST_BLOCK_FIRST) == first); + UNSCOPED_INFO("the second at the next"); + CHECK(mCompute.getStorageBinding(TEST_BLOCK_SECOND) == first + 1); + UNSCOPED_INFO("an undeclared block has none"); + CHECK(mCompute.getStorageBinding(TEST_COUNTS) == -1); + UNSCOPED_INFO("nor does an index past the table"); + CHECK(mCompute.getStorageBinding(TEST_STORAGE_END) == -1); const std::vector zeros(4, 0u); - ensure("a allocates", mBuffers[0].allocate(4 * sizeof(U32), 0, zeros.data(), "a (test)")); - ensure("b allocates", mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "b (test)")); + UNSCOPED_INFO("a allocates"); + CHECK(mBuffers[0].allocate(4 * sizeof(U32), 0, zeros.data(), "a (test)")); + UNSCOPED_INFO("b allocates"); + CHECK(mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "b (test)")); mCompute.bind(); mCompute.bindStorageBuffer(TEST_BLOCK_FIRST, mBuffers[0]); mCompute.bindStorageBuffer(TEST_BLOCK_SECOND, mBuffers[1]); mCompute.dispatch(1); - ensure_equals("a holds the first block", readWords(mBuffers[0], 1)[0], 111u); - ensure_equals("b holds the second block", readWords(mBuffers[1], 1)[0], 222u); + UNSCOPED_INFO("a holds the first block"); + CHECK(readWords(mBuffers[0], 1)[0] == 111u); + UNSCOPED_INFO("b holds the second block"); + CHECK(readWords(mBuffers[1], 1)[0] == 222u); mBuffers[0].clear(); mBuffers[1].clear(); @@ -1409,31 +1481,42 @@ namespace tut mCompute.bindStorageBuffer(TEST_BLOCK_FIRST, mBuffers[1]); mCompute.bindStorageBuffer(TEST_BLOCK_SECOND, mBuffers[0]); mCompute.dispatch(1); - ensure_equals("swapped, a holds the second block", readWords(mBuffers[0], 1)[0], 222u); - ensure_equals("swapped, b holds the first block", readWords(mBuffers[1], 1)[0], 111u); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("swapped, a holds the second block"); + CHECK(readWords(mBuffers[0], 1)[0] == 222u); + UNSCOPED_INFO("swapped, b holds the first block"); + CHECK(readWords(mBuffers[1], 1)[0] == 111u); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // Two image uniforms, declared the opposite way round, take distinct units in reserved order, and bindImage // routes each texture to its own. - template<> template<> - void llglslshader_object_t::test<17>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: image uniforms declared out of order take units in reserved order", "[llglslshader_gl]") { setUpCompute(); constexpr U32 SIDE = 16; - ensure("units builds", - buildProgram(mCompute, "image units (test)", { { "test/imageUnitsC.glsl", GL_COMPUTE_SHADER } })); - ensure_equals("the first reserved image first", mCompute.getImageUnit(TEST_IMAGE_A), 0); - ensure_equals("the second reserved image second", mCompute.getImageUnit(TEST_IMAGE_B), 1); - ensure_equals("an undeclared image has none", mCompute.getImageUnit(TEST_IMAGE_R32F), -1); - ensure_equals("a sampler has none", mCompute.getImageUnit(LLShaderMgr::DIFFUSE_MAP), -1); - ensure_equals("nor a texture unit", mCompute.getTextureChannel(TEST_IMAGE_A), -1); - - ensure("a allocates", mTargets[0].allocate(SIDE, SIDE, GL_R32F)); - ensure("b allocates", mTargets[1].allocate(SIDE, SIDE, GL_R32F)); + UNSCOPED_INFO("units builds"); + CHECK(buildProgram(mCompute, "image units (test)", { { "test/imageUnitsC.glsl", GL_COMPUTE_SHADER } })); + UNSCOPED_INFO("the first reserved image first"); + CHECK(mCompute.getImageUnit(TEST_IMAGE_A) == 0); + UNSCOPED_INFO("the second reserved image second"); + CHECK(mCompute.getImageUnit(TEST_IMAGE_B) == 1); + UNSCOPED_INFO("an undeclared image has none"); + CHECK(mCompute.getImageUnit(TEST_IMAGE_R32F) == -1); + UNSCOPED_INFO("a sampler has none"); + CHECK(mCompute.getImageUnit(LLShaderMgr::DIFFUSE_MAP) == -1); + UNSCOPED_INFO("nor a texture unit"); + CHECK(mCompute.getTextureChannel(TEST_IMAGE_A) == -1); + + UNSCOPED_INFO("a allocates"); + CHECK(mTargets[0].allocate(SIDE, SIDE, GL_R32F)); + UNSCOPED_INFO("b allocates"); + CHECK(mTargets[1].allocate(SIDE, SIDE, GL_R32F)); mCompute.bind(); - ensure_equals("a bound", mCompute.bindImage(TEST_IMAGE_A, &mTargets[0], ALImageSlot::WRITE_ONLY), 0); - ensure_equals("b bound", mCompute.bindImage(TEST_IMAGE_B, &mTargets[1], ALImageSlot::WRITE_ONLY), 1); + UNSCOPED_INFO("a bound"); + CHECK(mCompute.bindImage(TEST_IMAGE_A, &mTargets[0], ALImageSlot::WRITE_ONLY) == 0); + UNSCOPED_INFO("b bound"); + CHECK(mCompute.bindImage(TEST_IMAGE_B, &mTargets[1], ALImageSlot::WRITE_ONLY) == 1); mCompute.dispatchThreads(SIDE, SIDE); gGL.memoryBarrier(LLRender::BARRIER_TEXTURE_UPDATE); mCompute.unbindImage(TEST_IMAGE_A); @@ -1443,26 +1526,30 @@ namespace tut const std::vector b = readTexture(mTargets[1].getTexture(), SIDE, SIDE, 1, GL_RED, GL_FLOAT); for (U32 i = 0; i < SIDE * SIDE; ++i) { - ensure_equals("a holds image A at " + std::to_string(i), a[i], 1.f); - ensure_equals("b holds image B at " + std::to_string(i), b[i], 2.f); + UNSCOPED_INFO("a holds image A at " + std::to_string(i)); + REQUIRE(a[i] == 1.f); + UNSCOPED_INFO("b holds image B at " + std::to_string(i)); + REQUIRE(b[i] == 2.f); } - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A usampler2D under a reserved name gets a texture unit, the one after a float sampler's, and reads an R32UI // texture's values exactly, past what eight bits could hold. - template<> template<> - void llglslshader_object_t::test<18>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: a usampler2D takes the unit after a float one and reads R32UI exactly", "[llglslshader_gl]") { setUpCompute(); constexpr U32 W = 13; constexpr U32 H = 7; - ensure("sample builds", - buildProgram(mCompute, "uint sample (test)", { { "test/uintSampleC.glsl", GL_COMPUTE_SHADER } })); + UNSCOPED_INFO("sample builds"); + CHECK(buildProgram(mCompute, "uint sample (test)", { { "test/uintSampleC.glsl", GL_COMPUTE_SHADER } })); const S32 float_unit = mCompute.getTextureChannel(LLShaderMgr::DIFFUSE_MAP); const S32 uint_unit = mCompute.getTextureChannel(TEST_UINT_MAP); - ensure_equals("the float sampler first", float_unit, 0); - ensure_equals("the unsigned one after it", uint_unit, 1); + UNSCOPED_INFO("the float sampler first"); + CHECK(float_unit == 0); + UNSCOPED_INFO("the unsigned one after it"); + CHECK(uint_unit == 1); std::vector values(W * H); for (U32 i = 0; i < W * H; ++i) @@ -1471,17 +1558,21 @@ namespace tut } mUintImage = new LLImageGL(W, H, 1, /*usemipmaps=*/false); mUintImage->setExplicitFormat(GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT); - ensure("R32UI image created", mUintImage->createGLTexture(0, reinterpret_cast(values.data()))); - ensure("RGBA8 target allocates", mTargets[1].allocate(W, H, GL_RGBA8)); - ensure("out allocates", mBuffers[0].allocate(W * H * sizeof(U32), 0, nullptr, "uint sample (test)")); + UNSCOPED_INFO("R32UI image created"); + CHECK(mUintImage->createGLTexture(0, reinterpret_cast(values.data()))); + UNSCOPED_INFO("RGBA8 target allocates"); + CHECK(mTargets[1].allocate(W, H, GL_RGBA8)); + UNSCOPED_INFO("out allocates"); + CHECK(mBuffers[0].allocate(W * H * sizeof(U32), 0, nullptr, "uint sample (test)")); mBuffers[0].clear(0xFFFFFFFFu); mCompute.bind(); mCompute.uniform1i(TEST_COUNT, 1); - ensure_equals("float texture bound", mCompute.bindTexture(LLShaderMgr::DIFFUSE_MAP, &mTargets[1], ALSamplers::PointClamp), + UNSCOPED_INFO("float texture bound"); + CHECK(mCompute.bindTexture(LLShaderMgr::DIFFUSE_MAP, &mTargets[1], ALSamplers::PointClamp) == float_unit); - ensure("unsigned texture bound", - gGL.getTextureSlot((U32)uint_unit)->bindSampled(mUintImage.get(), ALSamplers::PointClamp)); + UNSCOPED_INFO("unsigned texture bound"); + CHECK(gGL.getTextureSlot((U32)uint_unit)->bindSampled(mUintImage.get(), ALSamplers::PointClamp)); mCompute.bindStorageBuffer(TEST_COUNTS, mBuffers[0]); mCompute.dispatchThreads(W, H); mCompute.unbindTexture(LLShaderMgr::DIFFUSE_MAP); @@ -1490,36 +1581,43 @@ namespace tut const std::vector read = readWords(mBuffers[0], W * H); for (U32 i = 0; i < W * H; ++i) { - ensure_equals("texel " + std::to_string(i), read[i], values[i]); + UNSCOPED_INFO("texel " + std::to_string(i)); + REQUIRE(read[i] == values[i]); } - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // Integer targets read through bindTexture with no sampler named: an R32UI and an RG16I target, each written by a // program's integer output, come back texel for texel through a usampler2D and an isampler2D. A filtering sampler // leaves an integer texture incomplete, reading 0, so the default for an integer attachment is point, attachment 0 // included. - template<> template<> - void llglslshader_object_t::test<19>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: bindTexture with no sampler named point-samples integer targets", "[llglslshader_gl]") { setUpShaders(); constexpr U32 W = 11; constexpr U32 H = 6; - ensure("R32UI target allocates", mTargets[0].allocate(W, H, GL_R32UI)); - ensure("RG16I target allocates", mTargets[1].allocate(W, H, GL_RG16I)); - ensure("output target allocates", mTargets[2].allocate(W, H, GL_RGBA32UI)); - ensure("its signed attachment", mTargets[2].addColorAttachment(GL_RGBA32I)); - ensure("unsigned writer builds", - buildProgram(mDraw, "uint write (test)", + UNSCOPED_INFO("R32UI target allocates"); + CHECK(mTargets[0].allocate(W, H, GL_R32UI)); + UNSCOPED_INFO("RG16I target allocates"); + CHECK(mTargets[1].allocate(W, H, GL_RG16I)); + UNSCOPED_INFO("output target allocates"); + CHECK(mTargets[2].allocate(W, H, GL_RGBA32UI)); + UNSCOPED_INFO("its signed attachment"); + CHECK(mTargets[2].addColorAttachment(GL_RGBA32I)); + UNSCOPED_INFO("unsigned writer builds"); + CHECK(buildProgram(mDraw, "uint write (test)", { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/uintWriteF.glsl", GL_FRAGMENT_SHADER } })); - ensure("signed writer builds", - buildProgram(mDraw2, "int write (test)", + UNSCOPED_INFO("signed writer builds"); + CHECK(buildProgram(mDraw2, "int write (test)", { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/intWriteF.glsl", GL_FRAGMENT_SHADER } })); - ensure("reader builds", - buildProgram(mDraw3, "integer sample (test)", + UNSCOPED_INFO("reader builds"); + CHECK(buildProgram(mDraw3, "integer sample (test)", { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/intSampleF.glsl", GL_FRAGMENT_SHADER } })); - ensure("the unsigned sampler has a unit", mDraw3.getTextureChannel(TEST_UINT_MAP) >= 0); - ensure("the signed sampler has a unit", mDraw3.getTextureChannel(TEST_INT_MAP) >= 0); + UNSCOPED_INFO("the unsigned sampler has a unit"); + CHECK(mDraw3.getTextureChannel(TEST_UINT_MAP) >= 0); + UNSCOPED_INFO("the signed sampler has a unit"); + CHECK(mDraw3.getTextureChannel(TEST_INT_MAP) >= 0); LLGLDisable blend(GL_BLEND); for (U32 source = 0; source < 2; ++source) @@ -1532,8 +1630,10 @@ namespace tut mTargets[2].bindTarget(); mDraw3.bind(); - ensure("unsigned source bound", mDraw3.bindTexture(TEST_UINT_MAP, &mTargets[0]) >= 0); - ensure("signed source bound", mDraw3.bindTexture(TEST_INT_MAP, &mTargets[1]) >= 0); + UNSCOPED_INFO("unsigned source bound"); + CHECK(mDraw3.bindTexture(TEST_UINT_MAP, &mTargets[0]) >= 0); + UNSCOPED_INFO("signed source bound"); + CHECK(mDraw3.bindTexture(TEST_INT_MAP, &mTargets[1]) >= 0); // Read off GL, since a driver may read an integer texture under a filtering sampler texel by texel anyway. for (const S32 uniform : { (S32)TEST_UINT_MAP, (S32)TEST_INT_MAP }) @@ -1546,8 +1646,10 @@ namespace tut glGetSamplerParameteriv((GLuint)sampler, GL_TEXTURE_MIN_FILTER, &min_filter); glGetSamplerParameteriv((GLuint)sampler, GL_TEXTURE_MAG_FILTER, &mag_filter); const std::string which = uniform == TEST_UINT_MAP ? "the unsigned source's" : "the signed source's"; - ensure_equals(which + " sampler magnifies by the nearest texel", (U32)mag_filter, (U32)GL_NEAREST); - ensure_equals(which + " sampler minifies by the nearest texel", (U32)min_filter, + UNSCOPED_INFO(which + " sampler magnifies by the nearest texel"); + REQUIRE((U32)mag_filter == (U32)GL_NEAREST); + UNSCOPED_INFO(which + " sampler minifies by the nearest texel"); + REQUIRE((U32)min_filter == (U32)GL_NEAREST_MIPMAP_NEAREST); } glActiveTexture(GL_TEXTURE0 + gGL.getCurrentTexUnitIndex()); @@ -1558,8 +1660,10 @@ namespace tut mDraw3.bindTexture(TEST_UINT_MAP, &mTargets[0], ALSamplers::BilinearClamp); const bool filtered_valid = validate_bound_samplers(); gDebugGL = false; - ensure("the debug validator passes the binds", samplers_valid); - ensure("and refuses a filtering sampler over an integer texture", !filtered_valid); + UNSCOPED_INFO("the debug validator passes the binds"); + CHECK(samplers_valid); + UNSCOPED_INFO("and refuses a filtering sampler over an integer texture"); + CHECK_FALSE(filtered_valid); mDraw3.bindTexture(TEST_UINT_MAP, &mTargets[0]); gGL.drawArraysInstanced(LLRender::TRIANGLES, 0, 3, 1); @@ -1583,12 +1687,15 @@ namespace tut const S32 int_expected[4] = { -1 - (S32)x, (S32)y * 300 - 1000, 0, 1 }; for (U32 c = 0; c < 4; ++c) { - ensure_equals("unsigned component " + std::to_string(c) + at, uints[i + c], uint_expected[c]); - ensure_equals("signed component " + std::to_string(c) + at, ints[i + c], int_expected[c]); + UNSCOPED_INFO("unsigned component " + std::to_string(c) + at); + REQUIRE(uints[i + c] == uint_expected[c]); + UNSCOPED_INFO("signed component " + std::to_string(c) + at); + REQUIRE(ints[i + c] == int_expected[c]); } } } - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A shared object is the shader manager's, not the programs' that attach it. Two programs built through @@ -1598,16 +1705,17 @@ namespace tut // built after both are gone attaches the shared object again and links. One that fails to link deletes its own // objects all the same. clearShaderObjects deletes every shared object at once, as no linked program holds one, // and the programs draw on without them. - template<> template<> - void llglslshader_object_t::test<20>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: a shared object belongs to the shader manager and not to its programs", "[llglslshader_gl]") { setUpShaders(); constexpr U32 SIDE = 4; - ensure("output target allocates", mTargets[0].allocate(SIDE, SIDE, GL_RGBA32F)); + UNSCOPED_INFO("output target allocates"); + CHECK(mTargets[0].allocate(SIDE, SIDE, GL_RGBA32F)); const auto& fragments = mShaders.mShaderObjects[LLShaderMgr::STAGE_FRAGMENT]; const auto found = fragments.find("deferred/globalF.glsl"); - ensure("the shared object compiled", found != fragments.end()); + UNSCOPED_INFO("the shared object compiled"); + REQUIRE(found != fragments.end()); const GLuint shared = found->second; const std::initializer_list> files = @@ -1632,41 +1740,58 @@ namespace tut const F32 expected[4] = { 0.25f, 0.5f, 0.75f, 1.f }; for (size_t i = 0; i < drawn.size(); ++i) { - ensure_equals(what + ", component " + std::to_string(i), drawn[i], expected[i % 4]); + UNSCOPED_INFO(what + ", component " + std::to_string(i)); + REQUIRE(drawn[i] == expected[i % 4]); } }; - ensure("A builds", buildProgram(mDraw, "shared A (test)", files)); - ensure("B builds", buildProgram(mDraw2, "shared B (test)", files)); - ensure("A holds no object once linked", attachedNames(mDraw.mProgramObject).empty()); - ensure("nor does B", attachedNames(mDraw2.mProgramObject).empty()); - ensure_equals("their own objects deleted", LLShaderMgr::sLiveObjects, live); - ensure("a program's own stage is not shared", !fragments.contains("test/globalCallF.glsl")); - ensure("the shared object stays", glIsShader(shared) == GL_TRUE); - ensure("not even flagged for deletion", !flaggedForDeletion(shared)); + UNSCOPED_INFO("A builds"); + CHECK(buildProgram(mDraw, "shared A (test)", files)); + UNSCOPED_INFO("B builds"); + CHECK(buildProgram(mDraw2, "shared B (test)", files)); + UNSCOPED_INFO("A holds no object once linked"); + REQUIRE(attachedNames(mDraw.mProgramObject).empty()); + UNSCOPED_INFO("nor does B"); + REQUIRE(attachedNames(mDraw2.mProgramObject).empty()); + UNSCOPED_INFO("their own objects deleted"); + CHECK(LLShaderMgr::sLiveObjects == live); + UNSCOPED_INFO("a program's own stage is not shared"); + CHECK_FALSE(fragments.contains("test/globalCallF.glsl")); + UNSCOPED_INFO("the shared object stays"); + CHECK(glIsShader(shared) == GL_TRUE); + UNSCOPED_INFO("not even flagged for deletion"); + CHECK_FALSE(flaggedForDeletion(shared)); ensureDraws("A draws", mDraw); mDraw.unload(); - ensure("unloading A leaves the shared object", glIsShader(shared) == GL_TRUE && !flaggedForDeletion(shared)); + UNSCOPED_INFO("unloading A leaves the shared object"); + CHECK((glIsShader(shared) == GL_TRUE && !flaggedForDeletion(shared))); glValidateProgram(mDraw2.mProgramObject); GLint valid = GL_FALSE; glGetProgramiv(mDraw2.mProgramObject, GL_VALIDATE_STATUS, &valid); - ensure("B validates", valid == GL_TRUE); + UNSCOPED_INFO("B validates"); + CHECK(valid == GL_TRUE); ensureDraws("B draws", mDraw2); mDraw2.unload(); - ensure("C builds afterwards", buildProgram(mDraw3, "shared C (test)", files)); - ensure("C holds no object once linked", attachedNames(mDraw3.mProgramObject).empty()); + UNSCOPED_INFO("C builds afterwards"); + CHECK(buildProgram(mDraw3, "shared C (test)", files)); + UNSCOPED_INFO("C holds no object once linked"); + REQUIRE(attachedNames(mDraw3.mProgramObject).empty()); ensureDraws("C draws", mDraw3); // Without the shared object to call into, the stage cannot link, at any class. - ensure("D does not link", !buildProgram(mDraw, "unlinked D (test)", files, 0, /*attach_nothing=*/true)); - ensure_equals("and its own objects are deleted all the same", LLShaderMgr::sLiveObjects, live); + UNSCOPED_INFO("D does not link"); + CHECK_FALSE(buildProgram(mDraw, "unlinked D (test)", files, 0, /*attach_nothing=*/true)); + UNSCOPED_INFO("and its own objects are deleted all the same"); + CHECK(LLShaderMgr::sLiveObjects == live); S32 level = 1; const GLuint loose = mShaders.loadShaderFile("test/solidF.glsl", level, GL_FRAGMENT_SHADER); - ensure("an object no program holds", loose != 0 && glIsShader(loose) == GL_TRUE); - ensure_equals("counted live", LLShaderMgr::sLiveObjects, live + 1); + UNSCOPED_INFO("an object no program holds"); + CHECK((loose != 0 && glIsShader(loose) == GL_TRUE)); + UNSCOPED_INFO("counted live"); + CHECK(LLShaderMgr::sLiveObjects == live + 1); size_t held = 0; for (const auto& stage : mShaders.mShaderObjects) @@ -1676,13 +1801,18 @@ namespace tut mShaders.clearShaderObjects(); for (const auto& stage : mShaders.mShaderObjects) { - ensure("the manager holds no object", stage.empty()); + UNSCOPED_INFO("the manager holds no object"); + REQUIRE(stage.empty()); } - ensure("one no program ever held goes at once", glIsShader(loose) == GL_FALSE); - ensure("and one programs linked with", glIsShader(shared) == GL_FALSE); - ensure_equals("none counted live", LLShaderMgr::sLiveObjects, live + 1 - (U32)held); + UNSCOPED_INFO("one no program ever held goes at once"); + CHECK(glIsShader(loose) == GL_FALSE); + UNSCOPED_INFO("and one programs linked with"); + CHECK(glIsShader(shared) == GL_FALSE); + UNSCOPED_INFO("none counted live"); + CHECK(LLShaderMgr::sLiveObjects == live + 1 - (U32)held); ensureDraws("C still draws", mDraw3); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A buffer bound once at a reserved block's point serves every program after it: one program declaring the block @@ -1690,23 +1820,27 @@ namespace tut // between -- where a numbering by program would put the shared block at point 0 for the first, the second block's // place. And LLRender keeps the points: a bind of what a point holds issues nothing, which a bind made behind its // back shows, and refreshState puts back what it holds. - template<> template<> - void llglslshader_object_t::test<21>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: a buffer bound once at a block's point serves every program after it", "[llglslshader_gl]") { setUpCompute(); - ensure("first-only builds", - buildProgram(mCompute, "first only (test)", { { "test/firstOnlyC.glsl", GL_COMPUTE_SHADER } })); - ensure("first-and-counts builds", - buildProgram(mCompute2, "first and counts (test)", { { "test/firstAndCountsC.glsl", GL_COMPUTE_SHADER } })); + UNSCOPED_INFO("first-only builds"); + CHECK(buildProgram(mCompute, "first only (test)", { { "test/firstOnlyC.glsl", GL_COMPUTE_SHADER } })); + UNSCOPED_INFO("first-and-counts builds"); + CHECK(buildProgram(mCompute2, "first and counts (test)", { { "test/firstAndCountsC.glsl", GL_COMPUTE_SHADER } })); const S32 first = mShaders.getStorageBlockBinding(TEST_BLOCK_FIRST); const S32 counts = mShaders.getStorageBlockBinding(TEST_COUNTS); - ensure_equals("the first program's block at the table's point", mCompute.getStorageBinding(TEST_BLOCK_FIRST), first); - ensure_equals("the second's too", mCompute2.getStorageBinding(TEST_BLOCK_FIRST), first); - ensure("the two blocks apart", first != counts); + UNSCOPED_INFO("the first program's block at the table's point"); + CHECK(mCompute.getStorageBinding(TEST_BLOCK_FIRST) == first); + UNSCOPED_INFO("the second's too"); + CHECK(mCompute2.getStorageBinding(TEST_BLOCK_FIRST) == first); + UNSCOPED_INFO("the two blocks apart"); + CHECK(first != counts); const std::vector zeros(4, 0u); - ensure("first allocates", mBuffers[0].allocate(4 * sizeof(U32), 0, zeros.data(), "first (test)")); - ensure("counts allocate", mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "counts (test)")); + UNSCOPED_INFO("first allocates"); + CHECK(mBuffers[0].allocate(4 * sizeof(U32), 0, zeros.data(), "first (test)")); + UNSCOPED_INFO("counts allocate"); + CHECK(mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "counts (test)")); mCompute.bind(); gGL.bindStorageBuffer((U32)first, mBuffers[0]); @@ -1716,9 +1850,12 @@ namespace tut mCompute2.dispatch(1); const std::vector held_first = readWords(mBuffers[0], 2); - ensure_equals("the first program wrote the first block", held_first[0], 7u); - ensure_equals("the second program wrote it too", held_first[1], 9u); - ensure_equals("and its counts", readWords(mBuffers[1], 1)[0], 5u); + UNSCOPED_INFO("the first program wrote the first block"); + CHECK(held_first[0] == 7u); + UNSCOPED_INFO("the second program wrote it too"); + CHECK(held_first[1] == 9u); + UNSCOPED_INFO("and its counts"); + CHECK(readWords(mBuffers[1], 1)[0] == 5u); // Behind LLRender's back, then the bind it holds already: nothing issued, so GL keeps the other buffer. auto bound_at = [](S32 point) @@ -1729,90 +1866,114 @@ namespace tut }; glBindBufferBase(GL_SHADER_STORAGE_BUFFER, (GLuint)first, mBuffers[1].getGLName()); gGL.bindStorageBuffer((U32)first, mBuffers[0]); - ensure_equals("a bind of what the point holds issues nothing", bound_at(first), mBuffers[1].getGLName()); + UNSCOPED_INFO("a bind of what the point holds issues nothing"); + CHECK(bound_at(first) == mBuffers[1].getGLName()); gGL.refreshState(); - ensure_equals("refreshState puts back what it holds", bound_at(first), mBuffers[0].getGLName()); + UNSCOPED_INFO("refreshState puts back what it holds"); + CHECK(bound_at(first) == mBuffers[0].getGLName()); // A range, at a multiple of the context's alignment at least 16 bytes in, is a different bind from the whole // buffer. const size_t alignment = (size_t)gGLManager.mShaderStorageBufferOffsetAlignment; const size_t offset = (16 + alignment - 1) / alignment * alignment; ALGPUBuffer wide; - ensure("wide allocates", wide.allocate(offset + 4 * sizeof(U32), 0, nullptr, "wide (test)")); + UNSCOPED_INFO("wide allocates"); + REQUIRE(wide.allocate(offset + 4 * sizeof(U32), 0, nullptr, "wide (test)")); wide.clear(); mCompute.bind(); gGL.bindStorageBuffer((U32)first, wide, offset, 4 * sizeof(U32)); mCompute.dispatch(1); std::vector words((offset + 4 * sizeof(U32)) / sizeof(U32), 0xFFFFFFFFu); wide.read(0, words.data(), words.size() * sizeof(U32)); - ensure_equals("the range's first word written", words[offset / sizeof(U32)], 7u); - ensure_equals("the word before it not", words[offset / sizeof(U32) - 1], 0u); + UNSCOPED_INFO("the range's first word written"); + CHECK(words[offset / sizeof(U32)] == 7u); + UNSCOPED_INFO("the word before it not"); + CHECK(words[offset / sizeof(U32) - 1] == 0u); // Releasing a bound buffer forgets it. wide.release(); - ensure_equals("a released buffer leaves its point empty", gGL.getStorageBuffer((U32)first), 0u); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("a released buffer leaves its point empty"); + CHECK(gGL.getStorageBuffer((U32)first) == 0u); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // An array of blocks takes one point per element from the table's start for it, and bindStorageBuffer names the // element: each of two buffers takes its element's writes. A program reading only the second element finds it // at its point all the same, though GL may report no first element for it. - template<> template<> - void llglslshader_object_t::test<22>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: a block array takes consecutive points and binds buffers by element", "[llglslshader_gl]") { setUpCompute(); - ensure("array program builds", - buildProgram(mCompute, "block array (test)", { { "test/blockArrayC.glsl", GL_COMPUTE_SHADER } })); - ensure("second-element program builds", - buildProgram(mCompute2, "block array one (test)", { { "test/blockArrayOneC.glsl", GL_COMPUTE_SHADER } })); + UNSCOPED_INFO("array program builds"); + CHECK(buildProgram(mCompute, "block array (test)", { { "test/blockArrayC.glsl", GL_COMPUTE_SHADER } })); + UNSCOPED_INFO("second-element program builds"); + CHECK(buildProgram(mCompute2, "block array one (test)", { { "test/blockArrayOneC.glsl", GL_COMPUTE_SHADER } })); const S32 base = mShaders.getStorageBlockBinding(TEST_BLOCK_ARRAY); - ensure("the table gives the array a point", base >= 0); - ensure_equals("element 0 at it", mCompute.getStorageBinding(TEST_BLOCK_ARRAY, 0), base); - ensure_equals("element 1 at the next", mCompute.getStorageBinding(TEST_BLOCK_ARRAY, 1), base + 1); - ensure_equals("no element 2", mCompute.getStorageBinding(TEST_BLOCK_ARRAY, 2), -1); - ensure_equals("the second-element program's element 1 there too", mCompute2.getStorageBinding(TEST_BLOCK_ARRAY, 1), + UNSCOPED_INFO("the table gives the array a point"); + CHECK(base >= 0); + UNSCOPED_INFO("element 0 at it"); + CHECK(mCompute.getStorageBinding(TEST_BLOCK_ARRAY, 0) == base); + UNSCOPED_INFO("element 1 at the next"); + CHECK(mCompute.getStorageBinding(TEST_BLOCK_ARRAY, 1) == base + 1); + UNSCOPED_INFO("no element 2"); + CHECK(mCompute.getStorageBinding(TEST_BLOCK_ARRAY, 2) == -1); + UNSCOPED_INFO("the second-element program's element 1 there too"); + CHECK(mCompute2.getStorageBinding(TEST_BLOCK_ARRAY, 1) == base + 1); const std::vector zeros(4, 0u); - ensure("a allocates", mBuffers[0].allocate(4 * sizeof(U32), 0, zeros.data(), "element 0 (test)")); - ensure("b allocates", mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "element 1 (test)")); + UNSCOPED_INFO("a allocates"); + CHECK(mBuffers[0].allocate(4 * sizeof(U32), 0, zeros.data(), "element 0 (test)")); + UNSCOPED_INFO("b allocates"); + CHECK(mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "element 1 (test)")); mCompute.bind(); mCompute.bindStorageBuffer(TEST_BLOCK_ARRAY, mBuffers[0], 0); mCompute.bindStorageBuffer(TEST_BLOCK_ARRAY, mBuffers[1], 1); mCompute.dispatch(1); - ensure_equals("element 0's buffer", readWords(mBuffers[0], 1)[0], 11u); - ensure_equals("element 1's buffer", readWords(mBuffers[1], 1)[0], 22u); + UNSCOPED_INFO("element 0's buffer"); + CHECK(readWords(mBuffers[0], 1)[0] == 11u); + UNSCOPED_INFO("element 1's buffer"); + CHECK(readWords(mBuffers[1], 1)[0] == 22u); mCompute2.bind(); mCompute2.dispatch(1); - ensure_equals("the second element's writer reaches element 1's buffer", readWords(mBuffers[1], 1)[0], 33u); - ensure_equals("and not element 0's", readWords(mBuffers[0], 1)[0], 11u); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("the second element's writer reaches element 1's buffer"); + CHECK(readWords(mBuffers[1], 1)[0] == 33u); + UNSCOPED_INFO("and not element 0's"); + CHECK(readWords(mBuffers[0], 1)[0] == 11u); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // An image array takes its units in reserved order like any image, though GL names it by its first element: it is // before testImageA in the table, so its two elements take units 0 and 1 and testImageA 2. bindImage names the // element, and each target takes its element's stores. - template<> template<> - void llglslshader_object_t::test<23>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: an image array takes its units in reserved order like any image", "[llglslshader_gl]") { setUpCompute(); constexpr U32 SIDE = 8; - ensure("image array builds", - buildProgram(mCompute, "image array (test)", { { "test/imageArrayC.glsl", GL_COMPUTE_SHADER } })); - ensure_equals("element 0 first", mCompute.getImageUnit(TEST_IMAGES, 0), 0); - ensure_equals("element 1 next", mCompute.getImageUnit(TEST_IMAGES, 1), 1); - ensure_equals("the single image after the array", mCompute.getImageUnit(TEST_IMAGE_A), 2); + UNSCOPED_INFO("image array builds"); + CHECK(buildProgram(mCompute, "image array (test)", { { "test/imageArrayC.glsl", GL_COMPUTE_SHADER } })); + UNSCOPED_INFO("element 0 first"); + CHECK(mCompute.getImageUnit(TEST_IMAGES, 0) == 0); + UNSCOPED_INFO("element 1 next"); + CHECK(mCompute.getImageUnit(TEST_IMAGES, 1) == 1); + UNSCOPED_INFO("the single image after the array"); + CHECK(mCompute.getImageUnit(TEST_IMAGE_A) == 2); for (U32 i = 0; i < 3; ++i) { - ensure("target allocates", mTargets[i].allocate(SIDE, SIDE, GL_R32F)); + UNSCOPED_INFO("target allocates"); + REQUIRE(mTargets[i].allocate(SIDE, SIDE, GL_R32F)); } mCompute.bind(); - ensure_equals("A bound", mCompute.bindImage(TEST_IMAGE_A, &mTargets[0], ALImageSlot::WRITE_ONLY), 2); - ensure_equals("element 0 bound", mCompute.bindImage(TEST_IMAGES, &mTargets[1], ALImageSlot::WRITE_ONLY, 0, 0, 0), 0); - ensure_equals("element 1 bound", mCompute.bindImage(TEST_IMAGES, &mTargets[2], ALImageSlot::WRITE_ONLY, 0, 0, 1), 1); + UNSCOPED_INFO("A bound"); + CHECK(mCompute.bindImage(TEST_IMAGE_A, &mTargets[0], ALImageSlot::WRITE_ONLY) == 2); + UNSCOPED_INFO("element 0 bound"); + CHECK(mCompute.bindImage(TEST_IMAGES, &mTargets[1], ALImageSlot::WRITE_ONLY, 0, 0, 0) == 0); + UNSCOPED_INFO("element 1 bound"); + CHECK(mCompute.bindImage(TEST_IMAGES, &mTargets[2], ALImageSlot::WRITE_ONLY, 0, 0, 1) == 1); mCompute.dispatchThreads(SIDE, SIDE); gGL.memoryBarrier(LLRender::BARRIER_TEXTURE_UPDATE); mCompute.unbindImage(TEST_IMAGE_A); @@ -1824,23 +1985,26 @@ namespace tut const std::vector held = readTexture(mTargets[t].getTexture(), SIDE, SIDE, 1, GL_RED, GL_FLOAT); for (U32 i = 0; i < SIDE * SIDE; ++i) { - ensure_equals("target " + std::to_string(t) + " at " + std::to_string(i), held[i], expected[t]); + UNSCOPED_INFO("target " + std::to_string(t) + " at " + std::to_string(i)); + REQUIRE(held[i] == expected[t]); } } - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // The debug validator reads image units as it reads texture units: a signed integer image over a float format is // undefined and refused, over a signed integer one passed, and an empty unit is fine. - template<> template<> - void llglslshader_object_t::test<24>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: the debug validator reads image units as it reads texture units", "[llglslshader_gl]") { setUpCompute(); constexpr U32 SIDE = 4; - ensure("signed image builds", - buildProgram(mCompute, "int image (test)", { { "test/imageIntC.glsl", GL_COMPUTE_SHADER } })); - ensure("float target allocates", mTargets[0].allocate(SIDE, SIDE, GL_R32F)); - ensure("signed target allocates", mTargets[1].allocate(SIDE, SIDE, GL_R32I)); + UNSCOPED_INFO("signed image builds"); + CHECK(buildProgram(mCompute, "int image (test)", { { "test/imageIntC.glsl", GL_COMPUTE_SHADER } })); + UNSCOPED_INFO("float target allocates"); + CHECK(mTargets[0].allocate(SIDE, SIDE, GL_R32F)); + UNSCOPED_INFO("signed target allocates"); + CHECK(mTargets[1].allocate(SIDE, SIDE, GL_R32I)); mCompute.bind(); gDebugGL = true; @@ -1852,28 +2016,31 @@ namespace tut gDebugGL = false; mCompute.unbindImage(TEST_IMAGE_INT); - ensure("an empty image unit passes", empty_valid); - ensure("a signed image over a float format is refused", !float_valid); - ensure("over a signed format it passes", signed_valid); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("an empty image unit passes"); + CHECK(empty_valid); + UNSCOPED_INFO("a signed image over a float format is refused"); + CHECK_FALSE(float_valid); + UNSCOPED_INFO("over a signed format it passes"); + CHECK(signed_valid); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A depth-stencil texture reads as its depth until GL 4.3's stencil texturing turns it to its stencil: the debug // validator refuses an unsigned sampler over it as depth and passes it as stencil, and the reverse for a float // sampler. - template<> template<> - void llglslshader_object_t::test<25>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: the debug validator reads a depth-stencil texture by its texture mode", "[llglslshader_gl]") { setUpShaders(); if (gGLManager.mGLVersion < 4.29f) { - skip("the context has no stencil texturing"); + SKIP("the context has no stencil texturing"); } - ensure("unsigned reader builds", - buildProgram(mDraw, "stencil read (test)", + UNSCOPED_INFO("unsigned reader builds"); + CHECK(buildProgram(mDraw, "stencil read (test)", { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/intSampleF.glsl", GL_FRAGMENT_SHADER } })); - ensure("float reader builds", - buildProgram(mDraw2, "depth read (test)", + UNSCOPED_INFO("float reader builds"); + CHECK(buildProgram(mDraw2, "depth read (test)", { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/sampleF.glsl", GL_FRAGMENT_SHADER } })); GLuint texture = 0; @@ -1901,36 +2068,46 @@ namespace tut return valid; }; - ensure("an unsigned sampler over the depth refused", - !validates(mDraw, TEST_UINT_MAP, GL_DEPTH_COMPONENT)); - ensure("over the stencil passed", validates(mDraw, TEST_UINT_MAP, GL_STENCIL_INDEX)); - ensure("a float sampler over the stencil refused", !validates(mDraw2, TEST_SOURCE_MAP, GL_STENCIL_INDEX)); - ensure("over the depth passed", validates(mDraw2, TEST_SOURCE_MAP, GL_DEPTH_COMPONENT)); + UNSCOPED_INFO("an unsigned sampler over the depth refused"); + CHECK_FALSE(validates(mDraw, TEST_UINT_MAP, GL_DEPTH_COMPONENT)); + UNSCOPED_INFO("over the stencil passed"); + CHECK(validates(mDraw, TEST_UINT_MAP, GL_STENCIL_INDEX)); + UNSCOPED_INFO("a float sampler over the stencil refused"); + CHECK_FALSE(validates(mDraw2, TEST_SOURCE_MAP, GL_STENCIL_INDEX)); + UNSCOPED_INFO("over the depth passed"); + CHECK(validates(mDraw2, TEST_SOURCE_MAP, GL_DEPTH_COMPONENT)); glDeleteTextures(1, &texture); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // Every kind of sampler takes a texture unit, in reserved order, an array included under its bare name though GL // names it by its first element: the two-element array first, then the array texture, the unsigned sampler and // the integer buffer. - template<> template<> - void llglslshader_object_t::test<26>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: every kind of sampler takes a texture unit in reserved order", "[llglslshader_gl]") { setUpShaders(); - ensure("sampler types build", - buildProgram(mDraw, "sampler types (test)", + UNSCOPED_INFO("sampler types build"); + CHECK(buildProgram(mDraw, "sampler types (test)", { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/samplerTypesF.glsl", GL_FRAGMENT_SHADER } })); - ensure_equals("the array first", mDraw.getTextureChannel(TEST_SAMPLERS), 0); - ensure_equals("the array texture after its two units", mDraw.getTextureChannel(TEST_SOURCE_MAP), 2); - ensure_equals("the unsigned sampler next", mDraw.getTextureChannel(TEST_UINT_MAP), 3); - ensure_equals("the integer buffer last", mDraw.getTextureChannel(TEST_INT_MAP), 4); + UNSCOPED_INFO("the array first"); + CHECK(mDraw.getTextureChannel(TEST_SAMPLERS) == 0); + UNSCOPED_INFO("the array texture after its two units"); + CHECK(mDraw.getTextureChannel(TEST_SOURCE_MAP) == 2); + UNSCOPED_INFO("the unsigned sampler next"); + CHECK(mDraw.getTextureChannel(TEST_UINT_MAP) == 3); + UNSCOPED_INFO("the integer buffer last"); + CHECK(mDraw.getTextureChannel(TEST_INT_MAP) == 4); GLint units[2] = { -1, -1 }; glGetUniformiv(mDraw.mProgramObject, glGetUniformLocation(mDraw.mProgramObject, "testSamplers[1]"), &units[1]); glGetUniformiv(mDraw.mProgramObject, glGetUniformLocation(mDraw.mProgramObject, "testSamplers[0]"), &units[0]); - ensure_equals("the array's first element at its unit", units[0], 0); - ensure_equals("its second at the next", units[1], 1); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("the array's first element at its unit"); + CHECK(units[0] == 0); + UNSCOPED_INFO("its second at the next"); + CHECK(units[1] == 1); + UNSCOPED_INFO("no GL error"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } } diff --git a/indra/llrender/tests/llimagegl_test.cpp b/indra/llrender/tests/llimagegl_test.cpp index ebf4734f0e..cb06a60d99 100644 --- a/indra/llrender/tests/llimagegl_test.cpp +++ b/indra/llrender/tests/llimagegl_test.cpp @@ -40,7 +40,7 @@ #include "llimage.h" -#include "../test/lltut.h" +#include #include #include @@ -50,8 +50,6 @@ #include #include -namespace tut -{ namespace { struct llimagegl_data @@ -98,47 +96,41 @@ namespace } }; - typedef test_group llimagegl_test; - typedef llimagegl_test::object llimagegl_object; - tut::llimagegl_test llimagegl_testcase("LLImageGL"); -} // createGLTexture(LLImageRaw) allocates immutable storage under a // fresh GL name. After it succeeds the instance reports // getHasGLTexture() and a non-zero texname. - template<> template<> - void llimagegl_object::test<1>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: createGLTexture(LLImageRaw) makes a texture under a fresh GL name", "[LLImageGL]") { LLPointer raw = makeRaw(16, 16, 4, /*fill=*/0x80); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded", - img->createGLTexture(/*discard_level=*/0, raw.get())); - ensure("texname allocated", - img->getTexName() != 0); - ensure("getHasGLTexture reports true", - img->getHasGLTexture()); + UNSCOPED_INFO("createGLTexture succeeded"); + CHECK(img->createGLTexture(/*discard_level=*/0, raw.get())); + UNSCOPED_INFO("texname allocated"); + CHECK(img->getTexName() != 0); + UNSCOPED_INFO("getHasGLTexture reports true"); + CHECK(img->getHasGLTexture()); } // destroyGLTexture must drop the GL name. Subsequent reads see // mTexName == 0 and getHasGLTexture() == false; the LLImageGL // instance itself remains usable (could be re-uploaded). - template<> template<> - void llimagegl_object::test<2>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: destroyGLTexture drops the GL name", "[LLImageGL]") { LLPointer raw = makeRaw(16, 16, 4, /*fill=*/0x80); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded", - img->createGLTexture(0, raw.get())); - ensure("texname allocated before destroy", - img->getTexName() != 0); + UNSCOPED_INFO("createGLTexture succeeded"); + CHECK(img->createGLTexture(0, raw.get())); + UNSCOPED_INFO("texname allocated before destroy"); + CHECK(img->getTexName() != 0); img->destroyGLTexture(); - ensure_equals("texname cleared after destroy", - (S32)img->getTexName(), 0); - ensure("getHasGLTexture false after destroy", - !img->getHasGLTexture()); + UNSCOPED_INFO("texname cleared after destroy"); + CHECK((S32)img->getTexName() == 0); + UNSCOPED_INFO("getHasGLTexture false after destroy"); + CHECK_FALSE(img->getHasGLTexture()); } // Regression for 8e85c68d69 — the partial-rect setSubImage path @@ -146,13 +138,12 @@ namespace // batch flush bound to nothing (one-frame glyph flicker on cache // miss). Verify the GL texture binding survives a setSubImage that // skips unbind. - template<> template<> - void llimagegl_object::test<3>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a partial setSubImage leaves the texture bound to unit 0", "[LLImageGL]") { LLPointer raw = makeRaw(32, 32, 4, /*fill=*/0x80); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded", - img->createGLTexture(0, raw.get())); + UNSCOPED_INFO("createGLTexture succeeded"); + CHECK(img->createGLTexture(0, raw.get())); // Bind the new texture to unit 0 the same way the renderer // would, then snapshot what GL_TEXTURE_BINDING_2D points at. @@ -161,8 +152,8 @@ namespace GLint bound_before = 0; glGetIntegerv(GL_TEXTURE_BINDING_2D, &bound_before); - ensure_equals("texture is bound to unit 0 before setSubImage", - (S32)bound_before, (S32)img->getTexName()); + UNSCOPED_INFO("texture is bound to unit 0 before setSubImage"); + CHECK((S32)bound_before == (S32)img->getTexName()); // Partial-rect upload — this is the path that used to // disable() the unit on its way out. The source raw must @@ -171,8 +162,8 @@ namespace // offset, so a 32x32 source matching the destination is // simplest. LLPointer patch = makeRaw(32, 32, 4, /*fill=*/0xFF); - ensure("setSubImage succeeded", - img->setSubImage(patch.get(), + UNSCOPED_INFO("setSubImage succeeded"); + CHECK(img->setSubImage(patch.get(), /*x_pos=*/4, /*y_pos=*/4, /*width=*/8, /*height=*/8, /*force_fast_update=*/true, 0, true)); @@ -180,8 +171,8 @@ namespace glActiveTexture(GL_TEXTURE0); GLint bound_after = 0; glGetIntegerv(GL_TEXTURE_BINDING_2D, &bound_after); - ensure_equals("texture still bound to unit 0 after setSubImage", - (S32)bound_after, (S32)img->getTexName()); + UNSCOPED_INFO("texture still bound to unit 0 after setSubImage"); + CHECK((S32)bound_after == (S32)img->getTexName()); } // Regression for 444c29b161 — setExplicitFormat used to leave @@ -189,35 +180,34 @@ namespace // GL_LUMINANCE, GL_ALPHA) which then leaked into setSubImage / // readBackRaw and tripped GL_INVALID_ENUM in core. Now // resolveDeprecatedFormat rewrites them at format-set time. - template<> template<> - void llimagegl_object::test<4>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: setExplicitFormat rewrites deprecated formats and keeps modern ones", "[LLImageGL]") { if (gGLManager.mGLVersion < 3.29f) { - skip("resolveDeprecatedFormat is gated on GL >= 3.3"); + SKIP("resolveDeprecatedFormat is gated on GL >= 3.3"); return; } LLPointer la = new LLImageGL(/*usemipmaps=*/false); la->setExplicitFormat(GL_LUMINANCE_ALPHA, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE); - ensure_equals("LUMINANCE_ALPHA primary rewrites to GL_RG", - (S32)la->getPrimaryFormat(), (S32)GL_RG); + UNSCOPED_INFO("LUMINANCE_ALPHA primary rewrites to GL_RG"); + CHECK((S32)la->getPrimaryFormat() == (S32)GL_RG); LLPointer lum = new LLImageGL(/*usemipmaps=*/false); lum->setExplicitFormat(GL_LUMINANCE, GL_LUMINANCE, GL_UNSIGNED_BYTE); - ensure_equals("LUMINANCE primary rewrites to GL_RED", - (S32)lum->getPrimaryFormat(), (S32)GL_RED); + UNSCOPED_INFO("LUMINANCE primary rewrites to GL_RED"); + CHECK((S32)lum->getPrimaryFormat() == (S32)GL_RED); LLPointer alpha = new LLImageGL(/*usemipmaps=*/false); alpha->setExplicitFormat(GL_ALPHA, GL_ALPHA, GL_UNSIGNED_BYTE); - ensure_equals("ALPHA primary rewrites to GL_RED", - (S32)alpha->getPrimaryFormat(), (S32)GL_RED); + UNSCOPED_INFO("ALPHA primary rewrites to GL_RED"); + CHECK((S32)alpha->getPrimaryFormat() == (S32)GL_RED); // Modern formats shouldn't be touched. LLPointer rgba = new LLImageGL(/*usemipmaps=*/false); rgba->setExplicitFormat(GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE); - ensure_equals("RGBA primary preserved", - (S32)rgba->getPrimaryFormat(), (S32)GL_RGBA); + UNSCOPED_INFO("RGBA primary preserved"); + CHECK((S32)rgba->getPrimaryFormat() == (S32)GL_RGBA); } // Pure-CPU sanity: dataFormatBits/Bytes/Components match the @@ -225,74 +215,72 @@ namespace // multiple. These are static helpers; no GL state involved, but // they live here so the table stays close to the formats the // upload paths exercise. - template<> template<> - void llimagegl_object::test<5>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: dataFormatBits/Bytes/Components give the documented host and VRAM sizes", "[LLImageGL]") { - ensure_equals("RGBA8 -> 32 bits", - LLImageGL::dataFormatBits(GL_RGBA8), 32); - ensure_equals("RG8 -> 16 bits", - LLImageGL::dataFormatBits(GL_RG8), 16); - ensure_equals("R8 -> 8 bits", - LLImageGL::dataFormatBits(GL_R8), 8); - ensure_equals("RGBA16F -> 64 bits", - LLImageGL::dataFormatBits(GL_RGBA16F), 64); - - ensure_equals("RGBA components == 4", - LLImageGL::dataFormatComponents(GL_RGBA), 4); - ensure_equals("LUMINANCE_ALPHA components == 2", - LLImageGL::dataFormatComponents(GL_LUMINANCE_ALPHA), 2); - ensure_equals("RED components == 1", - LLImageGL::dataFormatComponents(GL_RED), 1); + UNSCOPED_INFO("RGBA8 -> 32 bits"); + CHECK(LLImageGL::dataFormatBits(GL_RGBA8) == 32); + UNSCOPED_INFO("RG8 -> 16 bits"); + CHECK(LLImageGL::dataFormatBits(GL_RG8) == 16); + UNSCOPED_INFO("R8 -> 8 bits"); + CHECK(LLImageGL::dataFormatBits(GL_R8) == 8); + UNSCOPED_INFO("RGBA16F -> 64 bits"); + CHECK(LLImageGL::dataFormatBits(GL_RGBA16F) == 64); + + UNSCOPED_INFO("RGBA components == 4"); + CHECK(LLImageGL::dataFormatComponents(GL_RGBA) == 4); + UNSCOPED_INFO("LUMINANCE_ALPHA components == 2"); + CHECK(LLImageGL::dataFormatComponents(GL_LUMINANCE_ALPHA) == 2); + UNSCOPED_INFO("RED components == 1"); + CHECK(LLImageGL::dataFormatComponents(GL_RED) == 1); // 16x16 RGBA8 = 16*16*4 = 1024 bytes, already 4-aligned. - ensure_equals("16x16 RGBA8 = 1024 bytes", - (S32)LLImageGL::dataFormatBytes(GL_RGBA8, 16, 16), 1024); + UNSCOPED_INFO("16x16 RGBA8 = 1024 bytes"); + CHECK((S32)LLImageGL::dataFormatBytes(GL_RGBA8, 16, 16) == 1024); // 5x3 R8 = 15 bytes; rounds up to 16 (4-aligned). - ensure_equals("5x3 R8 byte count rounds to 4-aligned 16", - (S32)LLImageGL::dataFormatBytes(GL_R8, 5, 3), 16); + UNSCOPED_INFO("5x3 R8 byte count rounds to 4-aligned 16"); + CHECK((S32)LLImageGL::dataFormatBytes(GL_R8, 5, 3) == 16); // Host vs. VRAM split: dataFormatBits returns the tight host // layout (used for CPU upload-buffer math); dataFormatVRAMBits // returns the padded driver allocation (used for VRAM stats). // For formats drivers don't pad, the two agree. - ensure_equals("RGB8 host -> 24 bits (tight)", - LLImageGL::dataFormatBits(GL_RGB8), 24); - ensure_equals("RGB8 VRAM -> 32 bits (padded to RGBX)", - LLImageGL::dataFormatVRAMBits(GL_RGB8), 32); - ensure_equals("RGB16F host -> 48 bits (tight)", - LLImageGL::dataFormatBits(GL_RGB16F), 48); - ensure_equals("RGB16F VRAM -> 64 bits (padded to RGBA16F)", - LLImageGL::dataFormatVRAMBits(GL_RGB16F), 64); - ensure_equals("RGB32F host -> 96 bits (tight)", - LLImageGL::dataFormatBits(GL_RGB32F), 96); - ensure_equals("RGB32F VRAM -> 128 bits (padded to RGBA32F)", - LLImageGL::dataFormatVRAMBits(GL_RGB32F), 128); - ensure_equals("DEPTH_COMPONENT24 host -> 24 bits (tight)", - LLImageGL::dataFormatBits(GL_DEPTH_COMPONENT24), 24); - ensure_equals("DEPTH_COMPONENT24 VRAM -> 32 bits (padded)", - LLImageGL::dataFormatVRAMBits(GL_DEPTH_COMPONENT24), 32); + UNSCOPED_INFO("RGB8 host -> 24 bits (tight)"); + CHECK(LLImageGL::dataFormatBits(GL_RGB8) == 24); + UNSCOPED_INFO("RGB8 VRAM -> 32 bits (padded to RGBX)"); + CHECK(LLImageGL::dataFormatVRAMBits(GL_RGB8) == 32); + UNSCOPED_INFO("RGB16F host -> 48 bits (tight)"); + CHECK(LLImageGL::dataFormatBits(GL_RGB16F) == 48); + UNSCOPED_INFO("RGB16F VRAM -> 64 bits (padded to RGBA16F)"); + CHECK(LLImageGL::dataFormatVRAMBits(GL_RGB16F) == 64); + UNSCOPED_INFO("RGB32F host -> 96 bits (tight)"); + CHECK(LLImageGL::dataFormatBits(GL_RGB32F) == 96); + UNSCOPED_INFO("RGB32F VRAM -> 128 bits (padded to RGBA32F)"); + CHECK(LLImageGL::dataFormatVRAMBits(GL_RGB32F) == 128); + UNSCOPED_INFO("DEPTH_COMPONENT24 host -> 24 bits (tight)"); + CHECK(LLImageGL::dataFormatBits(GL_DEPTH_COMPONENT24) == 24); + UNSCOPED_INFO("DEPTH_COMPONENT24 VRAM -> 32 bits (padded)"); + CHECK(LLImageGL::dataFormatVRAMBits(GL_DEPTH_COMPONENT24) == 32); // VRAM bytes path applies the same 4-byte alignment as the // host bytes path and routes unpadded formats through the // host table — so RGBA8 matches between the two. - ensure_equals("16x16 RGBA8 VRAM = 1024 bytes (matches host)", - (S32)LLImageGL::dataFormatVRAMBytes(GL_RGBA8, 16, 16), 1024); + UNSCOPED_INFO("16x16 RGBA8 VRAM = 1024 bytes (matches host)"); + CHECK((S32)LLImageGL::dataFormatVRAMBytes(GL_RGBA8, 16, 16) == 1024); // 16x16 RGB8: host 16*16*3 = 768; VRAM 16*16*4 = 1024. - ensure_equals("16x16 RGB8 host = 768 bytes", - (S32)LLImageGL::dataFormatBytes(GL_RGB8, 16, 16), 768); - ensure_equals("16x16 RGB8 VRAM = 1024 bytes (padded)", - (S32)LLImageGL::dataFormatVRAMBytes(GL_RGB8, 16, 16), 1024); + UNSCOPED_INFO("16x16 RGB8 host = 768 bytes"); + CHECK((S32)LLImageGL::dataFormatBytes(GL_RGB8, 16, 16) == 768); + UNSCOPED_INFO("16x16 RGB8 VRAM = 1024 bytes (padded)"); + CHECK((S32)LLImageGL::dataFormatVRAMBytes(GL_RGB8, 16, 16) == 1024); } // When no explicit format is set, createGLTexture derives one from // the raw's component count. With deprecated rewrites enabled this // means: 1 -> GL_RED, 2 -> GL_RG, 3 -> GL_RGB, 4 -> GL_RGBA. - template<> template<> - void llimagegl_object::test<6>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: createGLTexture picks the primary format from the component count", "[LLImageGL]") { if (gGLManager.mGLVersion < 3.29f) { - skip("resolveDeprecatedFormat is gated on GL >= 3.3"); + SKIP("resolveDeprecatedFormat is gated on GL >= 3.3"); return; } @@ -302,39 +290,39 @@ namespace // to 2 component"). LLPointer raw2 = makeRaw(8, 8, 2, /*fill=*/0x80); LLPointer img2 = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture 2-component succeeded", - img2->createGLTexture(0, raw2.get())); - ensure_equals("2 components -> GL_RG primary", - (S32)img2->getPrimaryFormat(), (S32)GL_RG); + UNSCOPED_INFO("createGLTexture 2-component succeeded"); + CHECK(img2->createGLTexture(0, raw2.get())); + UNSCOPED_INFO("2 components -> GL_RG primary"); + CHECK((S32)img2->getPrimaryFormat() == (S32)GL_RG); LLPointer raw4 = makeRaw(8, 8, 4, /*fill=*/0x80); LLPointer img4 = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture 4-component succeeded", - img4->createGLTexture(0, raw4.get())); - ensure_equals("4 components -> GL_RGBA primary", - (S32)img4->getPrimaryFormat(), (S32)GL_RGBA); + UNSCOPED_INFO("createGLTexture 4-component succeeded"); + CHECK(img4->createGLTexture(0, raw4.get())); + UNSCOPED_INFO("4 components -> GL_RGBA primary"); + CHECK((S32)img4->getPrimaryFormat() == (S32)GL_RGBA); } // usemipmaps=true must wire the auto-mipmap path through to GL — // verify by reading the bound texture's mip 1 dimensions back. Mip // 1 of an 8x8 base is 4x4; if mipmaps weren't generated the level // would have width 0. - template<> template<> - void llimagegl_object::test<7>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a texture made with usemipmaps has a mip chain in GL", "[LLImageGL]") { LLPointer raw = makeRaw(8, 8, 4, /*fill=*/0x80); LLPointer img = new LLImageGL(/*usemipmaps=*/true); - ensure("createGLTexture mipmapped succeeded", - img->createGLTexture(0, raw.get())); - ensure("getUseMipMaps reports true", - img->getUseMipMaps()); + UNSCOPED_INFO("createGLTexture mipmapped succeeded"); + CHECK(img->createGLTexture(0, raw.get())); + UNSCOPED_INFO("getUseMipMaps reports true"); + CHECK(img->getUseMipMaps()); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, img->getTexName()); GLint mip1_w = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 1, GL_TEXTURE_WIDTH, &mip1_w); - ensure_equals("mip 1 width == 4 for an 8x8 base", (S32)mip1_w, 4); + UNSCOPED_INFO("mip 1 width == 4 for an 8x8 base"); + CHECK((S32)mip1_w == 4); } // Regression for 2d4e2a60ea — GL_TEXTURE_SWIZZLE_RGBA must be set @@ -342,12 +330,11 @@ namespace // sees (R, R, R, A) for a 2-component LUMINANCE_ALPHA upload even // though the texture is now stored as GL_RG. Verifies the swizzle // mask matches applySwizzleForDeprecatedFormat's table. - template<> template<> - void llimagegl_object::test<8>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: createGLTexture sets the RRRA swizzle for luminance-alpha", "[LLImageGL]") { if (gGLManager.mGLVersion < 3.29f) { - skip("swizzle apply is gated on GL >= 3.3"); + SKIP("swizzle apply is gated on GL >= 3.3"); return; } @@ -356,8 +343,8 @@ namespace // createGLTexture then applies the matching swizzle. LLPointer raw = makeRaw(8, 8, 2, /*fill=*/0x80); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture 2-component succeeded", - img->createGLTexture(0, raw.get())); + UNSCOPED_INFO("createGLTexture 2-component succeeded"); + CHECK(img->createGLTexture(0, raw.get())); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, img->getTexName()); @@ -365,18 +352,21 @@ namespace GLint mask[4] = { 0, 0, 0, 0 }; glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA, mask); - ensure_equals("R swizzle == GL_RED", (S32)mask[0], (S32)GL_RED); - ensure_equals("G swizzle == GL_RED", (S32)mask[1], (S32)GL_RED); - ensure_equals("B swizzle == GL_RED", (S32)mask[2], (S32)GL_RED); - ensure_equals("A swizzle == GL_GREEN", (S32)mask[3], (S32)GL_GREEN); + UNSCOPED_INFO("R swizzle == GL_RED"); + CHECK((S32)mask[0] == (S32)GL_RED); + UNSCOPED_INFO("G swizzle == GL_RED"); + CHECK((S32)mask[1] == (S32)GL_RED); + UNSCOPED_INFO("B swizzle == GL_RED"); + CHECK((S32)mask[2] == (S32)GL_RED); + UNSCOPED_INFO("A swizzle == GL_GREEN"); + CHECK((S32)mask[3] == (S32)GL_GREEN); } // Round-trip: upload a known pattern, glGetTexImage it back, and // verify byte-for-byte. Covers the createGLTexture upload path and // readBackRaw together. RGBA chosen so no deprecated rewrite kicks // in — that path is covered separately by test<4> and test<6>. - template<> template<> - void llimagegl_object::test<9>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: readBackRaw returns the bytes createGLTexture uploaded", "[LLImageGL]") { constexpr U16 W = 8, H = 8; constexpr S8 C = 4; @@ -388,17 +378,17 @@ namespace sd[i] = (U8)(i & 0xFF); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded", - img->createGLTexture(0, src.get())); + UNSCOPED_INFO("createGLTexture succeeded"); + CHECK(img->createGLTexture(0, src.get())); LLPointer dst = new LLImageRaw(W, H, C); std::memset(dst->getData(), 0, (size_t)W * H * C); - ensure("readBackRaw succeeded", - img->readBackRaw(0, dst.get(), /*compressed_ok=*/false)); + UNSCOPED_INFO("readBackRaw succeeded"); + CHECK(img->readBackRaw(0, dst.get(), /*compressed_ok=*/false)); - ensure_equals("readback bytes match upload", - std::memcmp(src->getData(), dst->getData(), - (size_t)W * H * C), + UNSCOPED_INFO("readback bytes match upload"); + CHECK(std::memcmp(src->getData(), dst->getData(), + (size_t)W * H * C) == 0); } @@ -408,8 +398,7 @@ namespace // main thread on Windows off Intel, on Linux with NVIDIA, on macOS with AMD -- // stepped through rows by component count times a per-type width that had no // entry for it, and died. Where the slicing is off this passes either way. - template<> template<> - void llimagegl_object::test<10>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a packed pixel type uploads and updates as one value a pixel", "[LLImageGL]") { // 64 rows, a multiple of 32: the full upload takes the batched slices. constexpr U16 W = 64, H = 64; @@ -420,20 +409,21 @@ namespace LLPointer img = new LLImageGL(/*usemipmaps=*/false); img->setExplicitFormat(GL_RGBA8, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV); - ensure("createGLTexture succeeded", img->createGLTexture(0, src.get())); + UNSCOPED_INFO("createGLTexture succeeded"); + CHECK(img->createGLTexture(0, src.get())); std::vector got((size_t)W * H * 4); readTexture(img, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, got.data()); - ensure_equals("full upload reads back", - std::memcmp(got.data(), sd, got.size()), 0); + UNSCOPED_INFO("full upload reads back"); + REQUIRE(std::memcmp(got.data(), sd, got.size()) == 0); // A partial-width update, which slices one row at a time. LLPointer patch = new LLImageRaw(W, H, 4); U8* pd = patch->getData(); for (size_t i = 0; i < (size_t)W * H * 4; ++i) pd[i] = (U8)(0xFF - (i & 0xFF)); - ensure("setSubImage succeeded", - img->setSubImage(patch.get(), /*x_pos=*/8, /*y_pos=*/4, + UNSCOPED_INFO("setSubImage succeeded"); + CHECK(img->setSubImage(patch.get(), /*x_pos=*/8, /*y_pos=*/4, /*width=*/16, /*height=*/8, /*force_fast_update=*/true)); @@ -448,7 +438,8 @@ namespace match = std::memcmp(got.data() + at, (in_patch ? pd : sd) + at, 4) == 0; } } - ensure("the update lands where it was asked and nowhere else", match); + UNSCOPED_INFO("the update lands where it was asked and nowhere else"); + CHECK(match); } // scaleDown builds a new texture object, and texture-object state does not @@ -456,8 +447,7 @@ namespace // back red and luminance-alpha opaque. The FBO method -- the default -- could // not have kept the channels either, since it samples THROUGH the swizzle and // copies back by position, so these take the PBO copy whatever the setting. - template<> template<> - void llimagegl_object::test<11>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: scaleDown keeps a deprecated-format texture's swizzle and channels", "[LLImageGL]") { constexpr U16 W = 16, H = 16; LLPointer raw = new LLImageRaw(W, H, 2); @@ -469,26 +459,34 @@ namespace } LLPointer img = new LLImageGL(/*usemipmaps=*/true); - ensure("createGLTexture succeeded", img->createGLTexture(0, raw.get())); + UNSCOPED_INFO("createGLTexture succeeded"); + CHECK(img->createGLTexture(0, raw.get())); const U32 method = gGLManager.mDownScaleMethod; gGLManager.mDownScaleMethod = 0; const bool scaled = img->scaleDown(1); gGLManager.mDownScaleMethod = method; - ensure("scaleDown succeeded", scaled); - ensure_equals("discard level follows", img->getDiscardLevel(), 1); + UNSCOPED_INFO("scaleDown succeeded"); + CHECK(scaled); + UNSCOPED_INFO("discard level follows"); + CHECK(img->getDiscardLevel() == 1); bindForRead(img); GLint w = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); - ensure_equals("the new texture is half size", (S32)w, (S32)W / 2); + UNSCOPED_INFO("the new texture is half size"); + CHECK((S32)w == (S32)W / 2); GLint mask[4] = { 0, 0, 0, 0 }; glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA, mask); - ensure_equals("R swizzle == GL_RED", (S32)mask[0], (S32)GL_RED); - ensure_equals("G swizzle == GL_RED", (S32)mask[1], (S32)GL_RED); - ensure_equals("B swizzle == GL_RED", (S32)mask[2], (S32)GL_RED); - ensure_equals("A swizzle == GL_GREEN", (S32)mask[3], (S32)GL_GREEN); + UNSCOPED_INFO("R swizzle == GL_RED"); + CHECK((S32)mask[0] == (S32)GL_RED); + UNSCOPED_INFO("G swizzle == GL_RED"); + CHECK((S32)mask[1] == (S32)GL_RED); + UNSCOPED_INFO("B swizzle == GL_RED"); + CHECK((S32)mask[2] == (S32)GL_RED); + UNSCOPED_INFO("A swizzle == GL_GREEN"); + CHECK((S32)mask[3] == (S32)GL_GREEN); std::vector got((size_t)(W / 2) * (H / 2) * 2); glGetTexImage(GL_TEXTURE_2D, 0, GL_RG, GL_UNSIGNED_BYTE, got.data()); @@ -497,18 +495,18 @@ namespace { match = got[i] == 0x40 && got[i + 1] == 0xC0; } - ensure("luminance and alpha both survive the copy", match); + UNSCOPED_INFO("luminance and alpha both survive the copy"); + CHECK(match); } // scaleDown waits out an upload in flight: the LLImageGL thread is writing the // discard level and storage state it reads and replaces, and that upload's // publish would then install its texture under the discard level set here. - template<> template<> - void llimagegl_object::test<12>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: scaleDown waits until an upload in flight has ended", "[LLImageGL]") { LLPointer img = new LLImageGL(/*usemipmaps=*/true); - ensure("createGLTexture succeeded", - img->createGLTexture(0, makeRaw(16, 16, 4, 0x80).get())); + UNSCOPED_INFO("createGLTexture succeeded"); + CHECK(img->createGLTexture(0, makeRaw(16, 16, 4, 0x80).get())); const U32 name = img->getTexName(); // The PBO copy needs no framebuffer of its own. @@ -523,9 +521,12 @@ namespace gGLManager.mDownScaleMethod = method; - ensure("refused while an upload is in flight", !during); - ensure_equals("texture untouched meanwhile", (S32)name_during, (S32)name); - ensure("done once the upload has ended", after); + UNSCOPED_INFO("refused while an upload is in flight"); + CHECK_FALSE(during); + UNSCOPED_INFO("texture untouched meanwhile"); + CHECK((S32)name_during == (S32)name); + UNSCOPED_INFO("done once the upload has ended"); + CHECK(after); } // While an upload is in flight the members describe the texture being built. @@ -533,8 +534,7 @@ namespace // level, so it read the old texture's level 0 at the new texture's size. // Asset 64x64, on screen at discard 2 (a 16x16 texture), the upload in flight // building discard 0. - template<> template<> - void llimagegl_object::test<13>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: readBackRaw during an upload reads the texture on screen", "[LLImageGL]") { constexpr U16 W = 16, H = 16; LLPointer src = new LLImageRaw(W, H, 4); @@ -543,8 +543,10 @@ namespace sd[i] = (U8)(i & 0xFF); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded", img->createGLTexture(2, src.get())); - ensure_equals("on screen at 16 wide", img->getWidth(), 16); + UNSCOPED_INFO("createGLTexture succeeded"); + CHECK(img->createGLTexture(2, src.get())); + UNSCOPED_INFO("on screen at 16 wide"); + CHECK(img->getWidth() == 16); img->beginUpload(); img->setDiscardLevel(0); // what the worker's createGLTexture writes first @@ -555,116 +557,132 @@ namespace img->setDiscardLevel(2); img->endUpload(); - ensure("readBackRaw succeeded", read); - ensure_equals("read at the size on screen", (S32)dst->getWidth(), (S32)W); - ensure_equals("and reads what is on screen", - std::memcmp(dst->getData(), sd, (size_t)W * H * 4), 0); + UNSCOPED_INFO("readBackRaw succeeded"); + CHECK(read); + UNSCOPED_INFO("read at the size on screen"); + CHECK((S32)dst->getWidth() == (S32)W); + UNSCOPED_INFO("and reads what is on screen"); + CHECK(std::memcmp(dst->getData(), sd, (size_t)W * H * 4) == 0); } // The pick mask of a texture still being uploaded waits for that texture: // getMask answers for the one on screen, and off the main thread building the // mask in place freed the buffer getMask was reading. It publishes with the // texture, and an upload that never publishes leaves it alone. - template<> template<> - void llimagegl_object::test<14>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: the pick mask of an upload in flight publishes with its texture", "[LLImageGL]") { constexpr U16 W = 16, H = 16; LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded", - img->createGLTexture(0, makeRaw(W, H, 4, 0xFF).get())); + UNSCOPED_INFO("createGLTexture succeeded"); + CHECK(img->createGLTexture(0, makeRaw(W, H, 4, 0xFF).get())); const LLVector2 centre(0.5f, 0.5f); - ensure("opaque texture picks", img->getMask(centre)); + UNSCOPED_INFO("opaque texture picks"); + CHECK(img->getMask(centre)); LLPointer clear = makeRaw(W, H, 4, 0x00); img->beginUpload(); img->updatePickMask(W, H, clear->getData()); - ensure("the mask on screen answers while the upload is in flight", - img->getMask(centre)); + UNSCOPED_INFO("the mask on screen answers while the upload is in flight"); + CHECK(img->getMask(centre)); img->endUpload(); - ensure("an upload that never published leaves the mask alone", - img->getMask(centre)); + UNSCOPED_INFO("an upload that never published leaves the mask alone"); + CHECK(img->getMask(centre)); img->beginUpload(); img->updatePickMask(W, H, clear->getData()); img->syncTexName(img->getTexName()); - ensure("the new mask publishes with its texture", !img->getMask(centre)); + UNSCOPED_INFO("the new mask publishes with its texture"); + CHECK_FALSE(img->getMask(centre)); } // An edit writes through the ACTIVE unit, and a bind that finds the texture // already cached on slot 0 skips activating it. With another unit left active // the write went to whatever that unit held. The glyph atlas does exactly this: // still on slot 0 from the text being drawn when a new glyph arrives. - template<> template<> - void llimagegl_object::test<15>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: an edit reaches its own texture while another unit is active", "[LLImageGL]") { constexpr U16 W = 32, H = 32; LLPointer a = new LLImageGL(/*usemipmaps=*/false); LLPointer b = new LLImageGL(/*usemipmaps=*/false); - ensure("create a", a->createGLTexture(0, makeRaw(W, H, 4, 0x10).get())); - ensure("create b", b->createGLTexture(0, makeRaw(W, H, 4, 0x20).get())); + UNSCOPED_INFO("create a"); + CHECK(a->createGLTexture(0, makeRaw(W, H, 4, 0x10).get())); + UNSCOPED_INFO("create b"); + CHECK(b->createGLTexture(0, makeRaw(W, H, 4, 0x20).get())); // Partial update, as the glyph atlas does it. Forcing the slot 1 bind is what // leaves unit 1 active, cached or not. gGL.getTextureSlot(0)->bind(a.get()); gGL.getTextureSlot(1)->bind(b.get(), false, /*forceBind=*/true); - ensure("setSubImage succeeded", - a->setSubImage(makeRaw(W, H, 4, 0xFF).get(), 4, 4, 8, 8, + UNSCOPED_INFO("setSubImage succeeded"); + CHECK(a->setSubImage(makeRaw(W, H, 4, 0xFF).get(), 4, 4, 8, 8, /*force_fast_update=*/true)); - ensure_equals("the update reached a", readTexelRGBA(a, 5, 5), 0xFFFFFFFFu); - ensure_equals("and not b", readTexelRGBA(b, 5, 5), 0x20202020u); + UNSCOPED_INFO("the update reached a"); + CHECK(readTexelRGBA(a, 5, 5) == 0xFFFFFFFFu); + UNSCOPED_INFO("and not b"); + CHECK(readTexelRGBA(b, 5, 5) == 0x20202020u); // Re-upload in place, through setImage. gGL.getTextureSlot(0)->bind(a.get()); gGL.getTextureSlot(1)->bind(b.get(), false, /*forceBind=*/true); - ensure("re-upload succeeded", a->createGLTexture(0, makeRaw(W, H, 4, 0x77).get())); - ensure_equals("the re-upload reached a", readTexelRGBA(a, 5, 5), 0x77777777u); - ensure_equals("and not b", readTexelRGBA(b, 5, 5), 0x20202020u); + UNSCOPED_INFO("re-upload succeeded"); + CHECK(a->createGLTexture(0, makeRaw(W, H, 4, 0x77).get())); + UNSCOPED_INFO("the re-upload reached a"); + CHECK(readTexelRGBA(a, 5, 5) == 0x77777777u); + UNSCOPED_INFO("and not b"); + CHECK(readTexelRGBA(b, 5, 5) == 0x20202020u); } // A re-upload at the same discard level wrote into the live texture in place, // on the word of the discard level alone. Immutable storage cannot follow a new // size or format: the write was rejected, or dropped the channel that did not // fit, and the image went on describing a texture GL never had. - template<> template<> - void llimagegl_object::test<16>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a re-upload at the same discard level takes a new size or format", "[LLImageGL]") { LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("create 32x32", img->createGLTexture(0, makeRaw(32, 32, 4, 0x11).get())); - ensure("re-create 64x64", img->createGLTexture(0, makeRaw(64, 64, 4, 0x22).get())); + UNSCOPED_INFO("create 32x32"); + CHECK(img->createGLTexture(0, makeRaw(32, 32, 4, 0x11).get())); + UNSCOPED_INFO("re-create 64x64"); + CHECK(img->createGLTexture(0, makeRaw(64, 64, 4, 0x22).get())); bindForRead(img); GLint w = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); - ensure_equals("the texture took the new size", (S32)w, 64); - ensure_equals("and the new pixels", readTexelRGBA(img, 40, 40), 0x22222222u); + UNSCOPED_INFO("the texture took the new size"); + CHECK((S32)w == 64); + UNSCOPED_INFO("and the new pixels"); + CHECK(readTexelRGBA(img, 40, 40) == 0x22222222u); // Same size, an alpha channel the old storage has no room for. LLPointer rgb = new LLImageGL(/*usemipmaps=*/false); - ensure("create RGB", rgb->createGLTexture(0, makeRaw(16, 16, 3, 0x33).get())); - ensure("re-create RGBA", rgb->createGLTexture(0, makeRaw(16, 16, 4, 0x44).get())); + UNSCOPED_INFO("create RGB"); + CHECK(rgb->createGLTexture(0, makeRaw(16, 16, 3, 0x33).get())); + UNSCOPED_INFO("re-create RGBA"); + CHECK(rgb->createGLTexture(0, makeRaw(16, 16, 4, 0x44).get())); bindForRead(rgb); GLint format = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &format); - ensure_equals("the texture took the new format", (S32)format, (S32)GL_SRGB8_ALPHA8); - ensure_equals("and the new pixels", readTexelRGBA(rgb, 3, 3), 0x44444444u); + UNSCOPED_INFO("the texture took the new format"); + CHECK((S32)format == (S32)GL_SRGB8_ALPHA8); + UNSCOPED_INFO("and the new pixels"); + CHECK(readTexelRGBA(rgb, 3, 3) == 0x44444444u); } // A deleted texture's name may come back out of glGenTextures, and a slot whose // bind cache still held it would take the new texture for bound already and skip // the bind -- sampling texture 0 in its place, since deleting the old one put the // binding back to 0. The cache forgets the name when the texture goes. - template<> template<> - void llimagegl_object::test<17>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a slot's bind cache forgets a deleted texture's name", "[LLImageGL]") { LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded", - img->createGLTexture(0, makeRaw(16, 16, 4, 0x80).get())); + UNSCOPED_INFO("createGLTexture succeeded"); + CHECK(img->createGLTexture(0, makeRaw(16, 16, 4, 0x80).get())); const U32 name = img->getTexName(); gGL.getTextureSlot(3)->bind(img.get()); - ensure_equals("slot 3 caches it", gGL.getTextureSlot(3)->getCurrTexture(), name); + UNSCOPED_INFO("slot 3 caches it"); + CHECK(gGL.getTextureSlot(3)->getCurrTexture() == name); img->destroyGLTexture(); // However many frames the delete is held back for. @@ -673,44 +691,48 @@ namespace LLImageGL::updateClass(); } - ensure_equals("the cache let go of the deleted name", - gGL.getTextureSlot(3)->getCurrTexture(), 0u); + UNSCOPED_INFO("the cache let go of the deleted name"); + CHECK(gGL.getTextureSlot(3)->getCurrTexture() == 0u); } // Generated mips fill the whole pyramid the storage holds, and sampling can // reach it. MAX_LEVEL stayed at the discard levels -- five below 256x256 -- so // glGenerateMipmap stopped there and distant surfaces minified no further. - template<> template<> - void llimagegl_object::test<18>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: generated mips fill the whole pyramid and sampling reaches it", "[LLImageGL]") { constexpr U16 W = 256, H = 256; LLPointer img = new LLImageGL(/*usemipmaps=*/true); - ensure("createGLTexture succeeded", - img->createGLTexture(0, makeRaw(W, H, 4, 0x80).get())); + UNSCOPED_INFO("createGLTexture succeeded"); + CHECK(img->createGLTexture(0, makeRaw(W, H, 4, 0x80).get())); bindForRead(img); GLint max_level = 0; glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, &max_level); const S32 last = LLImageGL::calcMipLevelCount(W, H) - 1; - ensure_equals("sampling reaches the 1x1 level", (S32)max_level, last); + UNSCOPED_INFO("sampling reaches the 1x1 level"); + CHECK((S32)max_level == last); U8 px[4] = { 0, 0, 0, 0 }; glGetTexImage(GL_TEXTURE_2D, last, GL_RGBA, GL_UNSIGNED_BYTE, px); - ensure("and the 1x1 level was generated", - px[0] == 0x80 && px[1] == 0x80 && px[2] == 0x80 && px[3] == 0x80); + { + INFO("and the 1x1 level was generated"); + CHECK(px[0] == 0x80); + CHECK(px[1] == 0x80); + CHECK(px[2] == 0x80); + CHECK(px[3] == 0x80); + } } // gGL is thread_local and outlives a context, as it does across these tests, // and init left the slots' bind caches as the last context had them. A fresh // context hands out the same first names, so its first texture read as bound // already on a slot that had held its namesake, and the bind was skipped. - template<> template<> - void llimagegl_object::test<19>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a new context starts with clear bind caches and unit 0 active", "[LLImageGL]") { { LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded", - img->createGLTexture(0, makeRaw(16, 16, 4, 0x80).get())); + UNSCOPED_INFO("createGLTexture succeeded"); + CHECK(img->createGLTexture(0, makeRaw(16, 16, 4, 0x80).get())); gGL.getTextureSlot(2)->bind(img.get()); } @@ -718,26 +740,26 @@ namespace gl.reset(); gl = std::make_unique(); - ensure_equals("unit 2's cache starts empty", - gGL.getTextureSlot(2)->getCurrTexture(), 0u); - ensure_equals("and unit 0 is the active one", - gGL.getCurrentTexUnitIndex(), 0u); + UNSCOPED_INFO("unit 2's cache starts empty"); + CHECK(gGL.getTextureSlot(2)->getCurrTexture() == 0u); + UNSCOPED_INFO("and unit 0 is the active one"); + CHECK(gGL.getCurrentTexUnitIndex() == 0u); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded in the new context", - img->createGLTexture(0, makeRaw(16, 16, 4, 0x40).get())); + UNSCOPED_INFO("createGLTexture succeeded in the new context"); + CHECK(img->createGLTexture(0, makeRaw(16, 16, 4, 0x40).get())); gGL.getTextureSlot(2)->bind(img.get()); GLint bound = 0; glGetIntegerv(GL_TEXTURE_BINDING_2D, &bound); - ensure_equals("the bind reached GL", (U32)bound, img->getTexName()); + UNSCOPED_INFO("the bind reached GL"); + CHECK((U32)bound == img->getTexName()); } // The same for the blend and colour-mask caches. A context left blending // BT_ALPHA made init's own setSceneBlendType(BT_ALPHA) look redundant, so the // next context kept GL's ONE, ZERO while gGL believed it was alpha blending; // and a mask left off read as still off on a context writing every channel. - template<> template<> - void llimagegl_object::test<20>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a new context's blend and colour-mask caches match GL", "[LLImageGL]") { gGL.setSceneBlendType(LLRender::BT_ALPHA); gGL.setColorMask(false, false); @@ -750,27 +772,36 @@ namespace GLint dst = 0; glGetIntegerv(GL_BLEND_SRC_RGB, &src); glGetIntegerv(GL_BLEND_DST_RGB, &dst); - ensure_equals("init's alpha blending reached GL: source", - (S32)src, (S32)GL_SRC_ALPHA); - ensure_equals("init's alpha blending reached GL: destination", - (S32)dst, (S32)GL_ONE_MINUS_SRC_ALPHA); + UNSCOPED_INFO("init's alpha blending reached GL: source"); + CHECK((S32)src == (S32)GL_SRC_ALPHA); + UNSCOPED_INFO("init's alpha blending reached GL: destination"); + CHECK((S32)dst == (S32)GL_ONE_MINUS_SRC_ALPHA); bool mask[4] = { false, false, false, false }; gGL.getColorMask(mask); - ensure("the colour mask cache says what GL starts with", - mask[0] && mask[1] && mask[2] && mask[3]); + { + INFO("the colour mask cache says what GL starts with"); + CHECK(mask[0]); + CHECK(mask[1]); + CHECK(mask[2]); + CHECK(mask[3]); + } GLboolean gl_mask[4] = { GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE }; glGetBooleanv(GL_COLOR_WRITEMASK, gl_mask); - ensure("and GL agrees", - gl_mask[0] && gl_mask[1] && gl_mask[2] && gl_mask[3]); + { + INFO("and GL agrees"); + CHECK(gl_mask[0]); + CHECK(gl_mask[1]); + CHECK(gl_mask[2]); + CHECK(gl_mask[3]); + } } // And for vertex buffer names, which LLVertexBuffer pools per thread as // LLImageGL pools texture names. The pool was never emptied with its context, // so the next context bound a name it had never generated -- which a core // profile refuses -- and the buffer was never allocated. - template<> template<> - void llimagegl_object::test<21>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a new context binds vertex buffer names it made itself", "[LLImageGL]") { // A buffer of three vertices, written and unmapped as a caller does. Where the // name comes from the pool differs: the default VBO pool draws it when it @@ -779,9 +810,11 @@ namespace // after the unmap is the name this buffer drew from the pool. auto fill = [](LLVertexBuffer* vb, const std::string& when) { - ensure("allocateBuffer succeeded " + when, vb->allocateBuffer(3, 0)); + UNSCOPED_INFO("allocateBuffer succeeded " + when); + REQUIRE(vb->allocateBuffer(3, 0)); LLStrider verts; - ensure("getVertexStrider succeeded " + when, vb->getVertexStrider(verts)); + UNSCOPED_INFO("getVertexStrider succeeded " + when); + REQUIRE(vb->getVertexStrider(verts)); for (S32 i = 0; i < 3; ++i) { verts[i] = LLVector3((F32)i, 0.f, 0.f); @@ -803,8 +836,10 @@ namespace fill(vb, "in the new context"); GLint bound = 0; glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &bound); - ensure("a buffer is bound", bound != 0); - ensure("and it is one this context made", glIsBuffer((GLuint)bound) == GL_TRUE); + UNSCOPED_INFO("a buffer is bound"); + CHECK(bound != 0); + UNSCOPED_INFO("and it is one this context made"); + CHECK(glIsBuffer((GLuint)bound) == GL_TRUE); LLVertexBuffer::unbind(); } @@ -812,8 +847,7 @@ namespace // out the GPU: it fences, hands over, and moves on, and the main thread // publishes the texture once the fence has signalled. Until then the image // goes on showing the texture it had, and the caller's completion waits. - template<> template<> - void llimagegl_object::test<22>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: an upload on the LLImageGL thread publishes once its fence signals", "[LLImageGL]") { // The queue the upload thread hands its results back on, as the viewer's. // It has to exist before the image, which looks it up when built. @@ -830,8 +864,8 @@ namespace } thread_end; LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded", - img->createGLTexture(0, makeRaw(16, 16, 4, 0x11).get())); + UNSCOPED_INFO("createGLTexture succeeded"); + CHECK(img->createGLTexture(0, makeRaw(16, 16, 4, 0x11).get())); const U32 old_name = img->getTexName(); LLPointer raw = makeRaw(32, 32, 4, 0x55); @@ -853,8 +887,10 @@ namespace } // The handover was posted before worker_done was raised. mainloop.runPending(); - ensure("the worker finished", worker_done.load()); - ensure("and its createGLTexture succeeded", worker_ok.load()); + UNSCOPED_INFO("the worker finished"); + CHECK(worker_done.load()); + UNSCOPED_INFO("and its createGLTexture succeeded"); + CHECK(worker_ok.load()); bool completed = false; U32 name_at_completion = 0; @@ -864,9 +900,12 @@ namespace name_at_completion = img->getTexName(); }); - ensure("not published before its fence is seen", !completed); - ensure_equals("the texture on screen is the old one", img->getTexName(), old_name); - ensure_equals("at its own size", img->getWidth(), 16); + UNSCOPED_INFO("not published before its fence is seen"); + CHECK_FALSE(completed); + UNSCOPED_INFO("the texture on screen is the old one"); + CHECK(img->getTexName() == old_name); + UNSCOPED_INFO("at its own size"); + CHECK(img->getWidth() == 16); while (LLImageGL::publishUploads() > 0 && std::chrono::steady_clock::now() < deadline) { @@ -875,18 +914,22 @@ namespace LLImageGLThread::deleteSingleton(); - ensure("the completion ran", completed); - ensure("after the new texture published", name_at_completion != old_name); - ensure_equals("which is what the image names", img->getTexName(), name_at_completion); - ensure_equals("at its new size", img->getWidth(), 32); - ensure_equals("holding the upload", readTexelRGBA(img, 5, 5), 0x55555555u); + UNSCOPED_INFO("the completion ran"); + CHECK(completed); + UNSCOPED_INFO("after the new texture published"); + CHECK(name_at_completion != old_name); + UNSCOPED_INFO("which is what the image names"); + CHECK(img->getTexName() == name_at_completion); + UNSCOPED_INFO("at its new size"); + CHECK(img->getWidth() == 32); + UNSCOPED_INFO("holding the upload"); + CHECK(readTexelRGBA(img, 5, 5) == 0x55555555u); } // And the rest of what LLRender's setters cache. A polygon offset or patch // size the last context was left with made the same setting on the next one // look redundant, so the fresh context kept GL's initial value instead. - template<> template<> - void llimagegl_object::test<23>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a new context takes the polygon offset and patch size set on it", "[LLImageGL]") { gGL.setPolygonOffset(1.f, 2.f); gGL.setPatchVertices(4); @@ -903,19 +946,21 @@ namespace GLfloat units = 0.f; glGetFloatv(GL_POLYGON_OFFSET_FACTOR, &factor); glGetFloatv(GL_POLYGON_OFFSET_UNITS, &units); - ensure_equals("the polygon offset reached the new context: factor", (F32)factor, sign * 1.f); - ensure_equals("the polygon offset reached the new context: units", (F32)units, sign * 2.f); + UNSCOPED_INFO("the polygon offset reached the new context: factor"); + CHECK((F32)factor == sign * 1.f); + UNSCOPED_INFO("the polygon offset reached the new context: units"); + CHECK((F32)units == sign * 2.f); GLint patch = 0; glGetIntegerv(GL_PATCH_VERTICES, &patch); - ensure_equals("and the patch size", (S32)patch, 4); + UNSCOPED_INFO("and the patch size"); + CHECK((S32)patch == 4); } // Which vertex attribute arrays are enabled is vertex-array-object state, and // a fresh context's VAO has none. LLVertexBuffer's cache of them outlived the // context, so the next context's first draw skipped enabling the arrays it read. - template<> template<> - void llimagegl_object::test<24>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: setupClientArrays enables the position array again on a new context", "[LLImageGL]") { LLVertexBuffer::setupClientArrays(LLVertexBuffer::MAP_VERTEX); @@ -928,19 +973,20 @@ namespace glGetVertexAttribiv(LLVertexBuffer::TYPE_VERTEX, GL_VERTEX_ATTRIB_ARRAY_ENABLED, &enabled); LLVertexBuffer::setupClientArrays(0); - ensure("the position array is enabled on the new context", enabled == GL_TRUE); + UNSCOPED_INFO("the position array is enabled on the new context"); + CHECK(enabled == GL_TRUE); } // A worker ends by destroying its shared context on its own thread, where // it is current, as LLImageGLThread does. On EGL that only marked it for // destruction, and the context held its display past the window, so the // next window was made on the stale display, and failed. - template<> template<> - void llimagegl_object::test<25>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a window can be made after a worker destroyed its shared context", "[LLImageGL]") { LLWindow* window = gl->window(); void* shared = window->createSharedContext(); - ensure("a shared context was made", shared != nullptr); + UNSCOPED_INFO("a shared context was made"); + REQUIRE(shared != nullptr); std::thread worker([window, shared] { window->makeContextCurrent(shared); @@ -955,15 +1001,14 @@ namespace } catch (const std::exception& e) { - fail(std::string("the next window: ") + e.what()); + FAIL(std::string("the next window: ") + e.what()); } } // The integer formats through LLImageGL. R32UI, RG16I and RGBA32UI allocate as themselves, hold what was uploaded // bit for bit -- past 2^24, and below 0 -- and take a partial update; each is accounted at its texels' size; and // the one asked for with mips holds level 0 alone, since GL neither builds nor filters an integer texture's mips. - template<> template<> - void llimagegl_object::test<26>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: integer formats allocate as themselves and hold their texels exactly", "[LLImageGL]") { constexpr U16 W = 13, H = 7; constexpr size_t N = (size_t)W * H; @@ -976,23 +1021,29 @@ namespace LLPointer r32ui = new LLImageGL(W, H, 1, /*usemipmaps=*/true); r32ui->setExplicitFormat(GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT); U64 before = LLImageGL::getTextureBytesAllocated(); - ensure("R32UI created", r32ui->createGLTexture(0, reinterpret_cast(uints.data()))); - ensure_equals("R32UI accounted at four bytes a texel", - (S64)(LLImageGL::getTextureBytesAllocated() - before), (S64)N * 4); - ensure_equals("and its host bytes", (S64)r32ui->getMipBytes(0), (S64)N * 4); - ensure("no mips asked of it", !r32ui->getUseMipMaps()); + UNSCOPED_INFO("R32UI created"); + CHECK(r32ui->createGLTexture(0, reinterpret_cast(uints.data()))); + UNSCOPED_INFO("R32UI accounted at four bytes a texel"); + CHECK((S64)(LLImageGL::getTextureBytesAllocated() - before) == (S64)N * 4); + UNSCOPED_INFO("and its host bytes"); + CHECK((S64)r32ui->getMipBytes(0) == (S64)N * 4); + UNSCOPED_INFO("no mips asked of it"); + CHECK_FALSE(r32ui->getUseMipMaps()); bindForRead(r32ui); GLint format = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &format); - ensure_equals("R32UI storage", (S32)format, (S32)GL_R32UI); + UNSCOPED_INFO("R32UI storage"); + CHECK((S32)format == (S32)GL_R32UI); GLint level1_width = -1; glGetTexLevelParameteriv(GL_TEXTURE_2D, 1, GL_TEXTURE_WIDTH, &level1_width); - ensure_equals("no level past the first", (S32)level1_width, 0); + UNSCOPED_INFO("no level past the first"); + CHECK((S32)level1_width == 0); std::vector got_uints(N, 0); readTexture(r32ui, GL_RED_INTEGER, GL_UNSIGNED_INT, got_uints.data()); - ensure("R32UI holds the upload", got_uints == uints); + UNSCOPED_INFO("R32UI holds the upload"); + CHECK(got_uints == uints); // A partial update, its rows read from a wider source as the glyph atlas updates. std::vector patch(N); @@ -1000,8 +1051,8 @@ namespace { patch[i] = 0xF0000000u + (U32)i; } - ensure("R32UI partial update", - r32ui->setSubImage(reinterpret_cast(patch.data()), W, H, 2, 1, 4, 3, /*force_fast_update=*/true)); + UNSCOPED_INFO("R32UI partial update"); + CHECK(r32ui->setSubImage(reinterpret_cast(patch.data()), W, H, 2, 1, 4, 3, /*force_fast_update=*/true)); readTexture(r32ui, GL_RED_INTEGER, GL_UNSIGNED_INT, got_uints.data()); for (U32 y = 0; y < H; ++y) { @@ -1009,8 +1060,9 @@ namespace { const size_t i = (size_t)y * W + x; const bool in_patch = x >= 2 && x < 6 && y >= 1 && y < 4; - ensure_equals("R32UI texel " + std::to_string(x) + "," + std::to_string(y), got_uints[i], - in_patch ? patch[i] : uints[i]); + UNSCOPED_INFO("R32UI texel " + std::to_string(x) + "," + std::to_string(y)); + REQUIRE(got_uints[i] == + (in_patch ? patch[i] : uints[i])); } } @@ -1023,15 +1075,18 @@ namespace LLPointer rg16i = new LLImageGL(W, H, 2, /*usemipmaps=*/false); rg16i->setExplicitFormat(GL_RG16I, GL_RG_INTEGER, GL_SHORT); before = LLImageGL::getTextureBytesAllocated(); - ensure("RG16I created", rg16i->createGLTexture(0, reinterpret_cast(shorts.data()))); - ensure_equals("RG16I accounted at four bytes a texel", - (S64)(LLImageGL::getTextureBytesAllocated() - before), (S64)N * 4); + UNSCOPED_INFO("RG16I created"); + CHECK(rg16i->createGLTexture(0, reinterpret_cast(shorts.data()))); + UNSCOPED_INFO("RG16I accounted at four bytes a texel"); + CHECK((S64)(LLImageGL::getTextureBytesAllocated() - before) == (S64)N * 4); bindForRead(rg16i); glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &format); - ensure_equals("RG16I storage", (S32)format, (S32)GL_RG16I); + UNSCOPED_INFO("RG16I storage"); + CHECK((S32)format == (S32)GL_RG16I); std::vector got_shorts(N * 2, 0); readTexture(rg16i, GL_RG_INTEGER, GL_SHORT, got_shorts.data()); - ensure("RG16I holds the upload", got_shorts == shorts); + UNSCOPED_INFO("RG16I holds the upload"); + CHECK(got_shorts == shorts); std::vector wide(N * 4); for (size_t i = 0; i < N; ++i) @@ -1044,28 +1099,33 @@ namespace LLPointer rgba32ui = new LLImageGL(W, H, 4, /*usemipmaps=*/false); rgba32ui->setExplicitFormat(GL_RGBA32UI, GL_RGBA_INTEGER, GL_UNSIGNED_INT); before = LLImageGL::getTextureBytesAllocated(); - ensure("RGBA32UI created", rgba32ui->createGLTexture(0, reinterpret_cast(wide.data()))); - ensure_equals("RGBA32UI accounted at sixteen bytes a texel", - (S64)(LLImageGL::getTextureBytesAllocated() - before), (S64)N * 16); + UNSCOPED_INFO("RGBA32UI created"); + CHECK(rgba32ui->createGLTexture(0, reinterpret_cast(wide.data()))); + UNSCOPED_INFO("RGBA32UI accounted at sixteen bytes a texel"); + CHECK((S64)(LLImageGL::getTextureBytesAllocated() - before) == (S64)N * 16); bindForRead(rgba32ui); glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &format); - ensure_equals("RGBA32UI storage", (S32)format, (S32)GL_RGBA32UI); + UNSCOPED_INFO("RGBA32UI storage"); + CHECK((S32)format == (S32)GL_RGBA32UI); std::vector got_wide(N * 4, 0); readTexture(rgba32ui, GL_RGBA_INTEGER, GL_UNSIGNED_INT, got_wide.data()); - ensure("RGBA32UI holds the upload", got_wide == wide); + UNSCOPED_INFO("RGBA32UI holds the upload"); + CHECK(got_wide == wide); // What an LLImageRaw cannot hold, readBackRaw does not read: it has a byte a component. LLPointer raw = new LLImageRaw(); - ensure("no byte image of R32UI", !r32ui->readBackRaw(0, raw.get(), /*compressed_ok=*/false)); - ensure("nor a downscale", !r32ui->scaleDown(1)); + UNSCOPED_INFO("no byte image of R32UI"); + CHECK_FALSE(r32ui->readBackRaw(0, raw.get(), /*compressed_ok=*/false)); + UNSCOPED_INFO("nor a downscale"); + CHECK_FALSE(r32ui->scaleDown(1)); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + UNSCOPED_INFO("no GL error"); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } // The integer formats in the size and channel tables: the sized ones at their texels' bits, the three-channel ones // padded in VRAM as RGB8 is, and the pixel formats their data comes in at their channel counts. - template<> template<> - void llimagegl_object::test<27>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: the size and channel tables cover the integer formats", "[LLImageGL]") { const std::pair bits[] = { @@ -1080,9 +1140,11 @@ namespace for (const auto& entry : bits) { const std::string name = "format " + std::to_string(entry.first); - ensure_equals(name + " host bits", LLImageGL::dataFormatBits(entry.first), entry.second); + UNSCOPED_INFO(name + " host bits"); + REQUIRE(LLImageGL::dataFormatBits(entry.first) == entry.second); const S32 vram = entry.second == 24 ? 32 : entry.second == 48 ? 64 : entry.second == 96 ? 128 : entry.second; - ensure_equals(name + " VRAM bits", LLImageGL::dataFormatVRAMBits(entry.first), vram); + UNSCOPED_INFO(name + " VRAM bits"); + REQUIRE(LLImageGL::dataFormatVRAMBits(entry.first) == vram); } const std::pair components[] = @@ -1092,8 +1154,8 @@ namespace }; for (const auto& entry : components) { - ensure_equals("pixel format " + std::to_string(entry.first) + " components", - LLImageGL::dataFormatComponents(entry.first), entry.second); + UNSCOPED_INFO("pixel format " + std::to_string(entry.first) + " components"); + REQUIRE(LLImageGL::dataFormatComponents(entry.first) == entry.second); } } } diff --git a/indra/llrender/tests/llrender2dutils_gl_test.cpp b/indra/llrender/tests/llrender2dutils_gl_test.cpp index 2132f31368..b7b9548d47 100644 --- a/indra/llrender/tests/llrender2dutils_gl_test.cpp +++ b/indra/llrender/tests/llrender2dutils_gl_test.cpp @@ -33,15 +33,13 @@ #include "../llvertexbuffer.h" #include "llimage.h" -#include "../test/lltut.h" +#include #include #include #include #include -namespace tut -{ namespace { struct llrender2dutils_data @@ -123,28 +121,24 @@ namespace static void ensureNear(const std::string& what, F32 actual, F32 expected) { - ensure(what + ": " + std::to_string(actual) + " vs " + std::to_string(expected), - fabsf(actual - expected) < 1e-3f); + UNSCOPED_INFO(what + ": " + std::to_string(actual) + " vs " + std::to_string(expected)); + REQUIRE(fabsf(actual - expected) < 1e-3f); } }; - typedef test_group llrender2dutils_test; - typedef llrender2dutils_test::object llrender2dutils_object; - tut::llrender2dutils_test llrender2dutils_testcase("llrender2dutils_gl"); -} // A rotated image turns about its own centre and keeps its size. An // odd-sized image at a quarter turn covers the same pixels it does // unrotated -- an open folder arrow sits where the closed one did. - template<> template<> - void llrender2dutils_object::test<1>() + TEST_CASE_METHOD(llrender2dutils_data, "llrender2dutils_gl: a rotated image turns about its own centre and keeps its size", "[llrender2dutils_gl]") { LLPointer texture = makeTexture(4, 4); const Bounds flat = boundsOf([&]() { gl_draw_scaled_rotated_image(10, 20, 9, 9, 0.f, texture.get()); }); - ensure_equals("unrotated, one quad", flat.count, (size_t)6); + UNSCOPED_INFO("unrotated, one quad"); + REQUIRE(flat.count == (size_t)6); ensureNear("unrotated left", flat.min_x, 10.f); ensureNear("unrotated right", flat.max_x, 19.f); ensureNear("unrotated bottom", flat.min_y, 20.f); @@ -153,7 +147,8 @@ namespace const Bounds turned = boundsOf([&]() { gl_draw_scaled_rotated_image(10, 20, 9, 9, 90.f, texture.get()); }); - ensure_equals("turned, one quad", turned.count, (size_t)6); + UNSCOPED_INFO("turned, one quad"); + REQUIRE(turned.count == (size_t)6); ensureNear("turned left", turned.min_x, flat.min_x); ensureNear("turned right", turned.max_x, flat.max_x); ensureNear("turned bottom", turned.min_y, flat.min_y); @@ -162,15 +157,15 @@ namespace // The turn is a turn: at a quarter turn a wide image stands tall, and // it does so about the centre of the rect it was asked to fill. - template<> template<> - void llrender2dutils_object::test<2>() + TEST_CASE_METHOD(llrender2dutils_data, "llrender2dutils_gl: a quarter turn stands a wide image tall about the rect's centre", "[llrender2dutils_gl]") { LLPointer texture = makeTexture(4, 4); const Bounds turned = boundsOf([&]() { gl_draw_scaled_rotated_image(10, 20, 8, 2, 90.f, texture.get()); }); - ensure_equals("one quad", turned.count, (size_t)6); + UNSCOPED_INFO("one quad"); + REQUIRE(turned.count == (size_t)6); // Centre (14, 21); half extents swap to (1, 4). ensureNear("left", turned.min_x, 13.f); ensureNear("right", turned.max_x, 15.f); @@ -179,8 +174,7 @@ namespace } // Rectangles added to one batch are one draw, where gl_rect_2d makes // a draw of each; and they are the same rectangles. - template<> template<> - void llrender2dutils_object::test<3>() + TEST_CASE_METHOD(llrender2dutils_data, "llrender2dutils_gl: gl_rect_2d_in_batch draws the same rectangles as gl_rect_2d in one draw", "[llrender2dutils_gl]") { const auto count_draws = [](const std::function& draw) { std::list capture; @@ -210,9 +204,12 @@ namespace } gGL.end(); }); - ensure_equals("a draw apiece", apiece.first, (size_t)40); - ensure_equals("one draw in a batch", batched.first, (size_t)1); - ensure_equals("the same vertices", batched.second, apiece.second); + UNSCOPED_INFO("a draw apiece"); + CHECK(apiece.first == (size_t)40); + UNSCOPED_INFO("one draw in a batch"); + CHECK(batched.first == (size_t)1); + UNSCOPED_INFO("the same vertices"); + CHECK(batched.second == apiece.second); const Bounds b = boundsOf([] { gGL.getTextureSlot(0)->unbind(); @@ -231,8 +228,7 @@ namespace // nothing else: at the window's corner too, where a line along the // edge of a pixel landing outside the rect is not drawn at all; and // with the UI scaled up, a screen pixel thick, not a unit. - template<> template<> - void llrender2dutils_object::test<4>() + TEST_CASE_METHOD(llrender2dutils_data, "llrender2dutils_gl: a rect's outline lights only the pixels just inside its edges", "[llrender2dutils_gl]") { constexpr S32 W = ll_test::HeadlessGL::WIDTH; constexpr S32 H = ll_test::HeadlessGL::HEIGHT; @@ -264,7 +260,7 @@ namespace const U8 red = rgba[(static_cast(y) * W + x) * 4]; if ((red == 255) != edge) { - fail("at " + std::to_string(x) + "," + std::to_string(y) + " of the outline at " + std::to_string(rect.mLeft) + "," + + FAIL("at " + std::to_string(x) + "," + std::to_string(y) + " of the outline at " + std::to_string(rect.mLeft) + "," + std::to_string(rect.mBottom) + ", scaled " + std::to_string(scale) + ": " + std::to_string(red)); } } diff --git a/indra/llrender/tests/llrendermatrix_test.cpp b/indra/llrender/tests/llrendermatrix_test.cpp index 28c170bcaf..1c0bd30035 100644 --- a/indra/llrender/tests/llrendermatrix_test.cpp +++ b/indra/llrender/tests/llrendermatrix_test.cpp @@ -32,13 +32,12 @@ #include "m4math.h" #include "llquaternion.h" -#include "../test/lltut.h" +#include +#include #include #include -namespace tut -{ namespace { struct llrendermatrix_data @@ -67,10 +66,6 @@ namespace } }; - typedef test_group llrendermatrix_test; - typedef llrendermatrix_test::object llrendermatrix_object; - tut::llrendermatrix_test llrendermatrix_testgroup("llrendermatrix"); - // Every element within tolerance of the larger magnitude, or of one. void ensure_matrix_close(const std::string& what, const LLMatrix4a& actual, const LLMatrix4a& expected, F32 tolerance) { @@ -81,7 +76,8 @@ namespace const F32 scale = llmax(1.f, fabsf(a[i]), fabsf(e[i])); std::ostringstream msg; msg << what << " at element " << i << ": " << a[i] << " vs " << e[i]; - ensure_approximately_equals_range(msg.str().c_str(), a[i], e[i], tolerance * scale); + UNSCOPED_INFO(msg.str().c_str()); + REQUIRE_THAT(a[i], Catch::Matchers::WithinAbs(e[i], tolerance * scale)); } } @@ -92,7 +88,8 @@ namespace const F32 scale = llmax(1.f, fabsf(actual[i]), fabsf(expected[i])); std::ostringstream msg; msg << what << " at lane " << i << ": " << actual[i] << " vs " << expected[i]; - ensure_approximately_equals_range(msg.str().c_str(), actual[i], expected[i], tolerance * scale); + UNSCOPED_INFO(msg.str().c_str()); + REQUIRE_THAT(actual[i], Catch::Matchers::WithinAbs(expected[i], tolerance * scale)); } } @@ -117,13 +114,11 @@ namespace -0.8f, 0.6f, 0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 5.f, -7.f, 9.f, 1.f }; -} // The stack ops apply the new transform ahead of what the stack held, // which on the scalar matrix is the product written the other way // round. - template<> template<> - void llrendermatrix_object::test<1>() + TEST_CASE_METHOD(llrendermatrix_data, "llrendermatrix: the stack ops apply the new transform ahead of what the stack held", "[llrendermatrix]") { gGL.loadMatrix(M_ROWS); gGL.translatef(1.f, 2.f, 3.f); @@ -151,18 +146,19 @@ namespace // push copies, pop restores gGL.pushMatrix(); gGL.loadIdentity(); - ensure("loadIdentity", gGL.getModelviewMatrix() == LLMatrix4a::identity()); + UNSCOPED_INFO("loadIdentity"); + CHECK(gGL.getModelviewMatrix() == LLMatrix4a::identity()); gGL.popMatrix(); ensure_matrix_close("popMatrix restores", gGL.getModelviewMatrix(), LLMatrix4a(expected), 1e-5f); // the modes are separate stacks gGL.matrixMode(LLRender::MM_PROJECTION); - ensure("the projection stack is untouched", gGL.getProjectionMatrix() == LLMatrix4a::identity()); + UNSCOPED_INFO("the projection stack is untouched"); + CHECK(gGL.getProjectionMatrix() == LLMatrix4a::identity()); } // ortho on the stack and the projection helpers, in both depth conventions. - template<> template<> - void llrendermatrix_object::test<2>() + TEST_CASE_METHOD(llrendermatrix_data, "llrendermatrix: the projection helpers and gGL.ortho under forward and reverse-Z", "[llrendermatrix]") { for (bool reverse : { false, true }) { @@ -174,17 +170,22 @@ namespace const LLMatrix4a expected_ortho = reverse ? reversed_z(ortho) : ortho; const LLMatrix4a expected_persp = reverse ? reversed_z(persp) : persp; - ensure("al_ortho" + when, same_bytes(al_ortho(-3.f, 5.f, -2.f, 7.f, 0.5f, 100.f), expected_ortho)); - ensure("al_perspective" + when, same_bytes(al_perspective(1.1f, 1.777f, 0.25f, 1024.f), expected_persp)); - ensure("al_reverse_z_transform" + when, same_bytes(al_reverse_z_transform(persp), reversed_z(persp))); + UNSCOPED_INFO("al_ortho" + when); + REQUIRE(same_bytes(al_ortho(-3.f, 5.f, -2.f, 7.f, 0.5f, 100.f), expected_ortho)); + UNSCOPED_INFO("al_perspective" + when); + REQUIRE(same_bytes(al_perspective(1.1f, 1.777f, 0.25f, 1024.f), expected_persp)); + UNSCOPED_INFO("al_reverse_z_transform" + when); + REQUIRE(same_bytes(al_reverse_z_transform(persp), reversed_z(persp))); // a reversed projection puts the near plane at window 1 and the // far at 0 LLVector4a near_clip, far_clip; expected_persp.transform4(LLVector4a(0.f, 0.f, -0.25f, 1.f), near_clip); expected_persp.transform4(LLVector4a(0.f, 0.f, -1024.f, 1.f), far_clip); - ensure_approximately_equals_range(("near plane depth" + when).c_str(), near_clip[2] / near_clip[3], reverse ? 1.f : -1.f, 1e-5f); - ensure_approximately_equals_range(("far plane depth" + when).c_str(), far_clip[2] / far_clip[3], reverse ? 0.f : 1.f, 1e-5f); + UNSCOPED_INFO(("near plane depth" + when).c_str()); + REQUIRE_THAT(near_clip[2] / near_clip[3], Catch::Matchers::WithinAbs(reverse ? 1.f : -1.f, 1e-5f)); + UNSCOPED_INFO(("far plane depth" + when).c_str()); + REQUIRE_THAT(far_clip[2] / far_clip[3], Catch::Matchers::WithinAbs(reverse ? 0.f : 1.f, 1e-5f)); gGL.matrixMode(LLRender::MM_PROJECTION); gGL.loadMatrix(M_ROWS); @@ -203,7 +204,11 @@ namespace const LLVector4a win = al_project(obj, mv, expected_persp, viewport); const LLVector4a expected_win = reverse ? alprojection::project_zo(obj, mv, expected_persp, viewport) : alprojection::project(obj, mv, expected_persp, viewport); ensure_vector_close("al_project" + when, win, expected_win, 1e-6f); - ensure("window depth is in [0, 1]" + when, win[2] >= 0.f && win[2] <= 1.f); + { + INFO("window depth is in [0, 1]" + when); + REQUIRE(win[2] >= 0.f); + REQUIRE(win[2] <= 1.f); + } const LLVector4a back = al_unproject(win, mv, expected_persp, viewport); ensure_vector_close("al_unproject of al_project" + when, back, obj, 5e-4f); @@ -218,8 +223,7 @@ namespace // The oblique clip plane: the plane through the inverse transpose of // the modelview then projection, normalized on depth, applied after // the projection as its depth. - template<> template<> - void llrendermatrix_object::test<4>() + TEST_CASE_METHOD(llrendermatrix_data, "llrendermatrix: LLGLUserClipPlane rewrites only the depth column and puts it back", "[llrendermatrix]") { const LLMatrix4a persp = LLMatrix4a::perspective(1.1f, 1.777f, 0.25f, 1024.f); LLMatrix4a mv; @@ -257,10 +261,12 @@ namespace { for (S32 row = 0; row < 4; ++row) { - ensure_equals("the other columns are the projection's", gGL.getProjectionMatrix().mMatrix[row][column], persp.mMatrix[row][column]); + UNSCOPED_INFO("the other columns are the projection's"); + REQUIRE(gGL.getProjectionMatrix().mMatrix[row][column] == persp.mMatrix[row][column]); } } } - ensure("projection restored", same_bytes(gGL.getProjectionMatrix(), persp)); + UNSCOPED_INFO("projection restored"); + CHECK(same_bytes(gGL.getProjectionMatrix(), persp)); } } diff --git a/indra/llrender/tests/llrendertarget_gl_test.cpp b/indra/llrender/tests/llrendertarget_gl_test.cpp index 6f3126b69f..e077ac1405 100644 --- a/indra/llrender/tests/llrendertarget_gl_test.cpp +++ b/indra/llrender/tests/llrendertarget_gl_test.cpp @@ -32,16 +32,14 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include #include #include -namespace tut +namespace { - namespace - { constexpr U32 SIZE = 4; // A triangle over the viewport. @@ -106,7 +104,8 @@ namespace tut { for (size_t i = 0; i < texels.size(); ++i) { - ensure_equals(what + ", texel " + std::to_string(i / 4) + " channel " + std::to_string(i % 4), texels[i], + UNSCOPED_INFO(what + ", texel " + std::to_string(i / 4) + " channel " + std::to_string(i % 4)); + REQUIRE(texels[i] == expected[i % 4]); } } @@ -147,7 +146,11 @@ namespace tut { const GLuint vs = ll_test::compileTestShader(GL_VERTEX_SHADER, kVertex); const GLuint fs = ll_test::compileTestShader(GL_FRAGMENT_SHADER, kIntegerFragment); - ensure("integer shaders compile", vs && fs); + { + INFO("integer shaders compile"); + REQUIRE(vs); + REQUIRE(fs); + } const GLuint program = glCreateProgram(); glAttachShader(program, vs); glAttachShader(program, fs); @@ -156,7 +159,8 @@ namespace tut glDeleteShader(fs); GLint linked = GL_FALSE; glGetProgramiv(program, GL_LINK_STATUS, &linked); - ensure("integer program links", linked == GL_TRUE); + UNSCOPED_INFO("integer program links"); + REQUIRE(linked == GL_TRUE); return program; } @@ -164,7 +168,11 @@ namespace tut { const GLuint vs = ll_test::compileTestShader(GL_VERTEX_SHADER, kVertex); const GLuint fs = ll_test::compileTestShader(GL_FRAGMENT_SHADER, kFragment); - ensure("shaders compile", vs && fs); + { + INFO("shaders compile"); + REQUIRE(vs); + REQUIRE(fs); + } mProgram = glCreateProgram(); glAttachShader(mProgram, vs); glAttachShader(mProgram, fs); @@ -173,10 +181,12 @@ namespace tut glDeleteShader(fs); GLint linked = GL_FALSE; glGetProgramiv(mProgram, GL_LINK_STATUS, &linked); - ensure("program links", linked == GL_TRUE); + UNSCOPED_INFO("program links"); + REQUIRE(linked == GL_TRUE); glGenVertexArrays(1, &mVAO); - ensure("target allocates", mTarget.allocate(SIZE, SIZE, GL_RGBA8)); + UNSCOPED_INFO("target allocates"); + REQUIRE(mTarget.allocate(SIZE, SIZE, GL_RGBA8)); } // What the target holds after the triangle draws into it, its draw buffers set to output `output`, or left as @@ -214,52 +224,56 @@ namespace tut GLuint mIntegerProgram = 0; }; - typedef test_group llrendertarget_t; - typedef llrendertarget_t::object llrendertarget_object_t; - tut::llrendertarget_t tut_llrendertarget("llrendertarget_gl"); - } // setDrawOutput: a single-attachment target takes the output it names and none of the others, and the next // bindTarget gives it output 0 again. - template<> template<> - void llrendertarget_object_t::test<1>() + TEST_CASE_METHOD(llrendertarget_data, "llrendertarget_gl: setDrawOutput sends the named output to a single-attachment target", "[llrendertarget_gl]") { setUp(); - ensure_equals("bound: output 0", drawn(-1), OUTPUT_COLOURS[0]); + UNSCOPED_INFO("bound: output 0"); + CHECK(drawn(-1) == OUTPUT_COLOURS[0]); for (S32 output = 0; output < 4; ++output) { - ensure_equals("output " + std::to_string(output), drawn(output), OUTPUT_COLOURS[output]); + UNSCOPED_INFO("output " + std::to_string(output)); + REQUIRE(drawn(output) == OUTPUT_COLOURS[output]); } - ensure_equals("rebound: output 0 again", drawn(-1), OUTPUT_COLOURS[0]); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + UNSCOPED_INFO("rebound: output 0 again"); + CHECK(drawn(-1) == OUTPUT_COLOURS[0]); + UNSCOPED_INFO("no GL error"); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } // Integer attachments: R32UI, RG16I and RGBA32UI allocate as themselves beside a depth buffer. clear takes each to // the clear colour rounded to integers -- an unsigned one held at 0 below and at its top above -- and leaves them // alone when only the depth is asked for; a program's integer outputs reach them exactly. - template<> template<> - void llrendertarget_object_t::test<2>() + TEST_CASE_METHOD(llrendertarget_data, "llrendertarget_gl: integer attachments keep their formats through clear and draw", "[llrendertarget_gl]") { mIntegerProgram = linkInteger(); glGenVertexArrays(1, &mVAO); - ensure("R32UI target allocates", mInteger.allocate(INT_W, INT_H, GL_R32UI, /*depth=*/true)); - ensure("RG16I attaches", mInteger.addColorAttachment(GL_RG16I)); - ensure("RGBA32UI attaches", mInteger.addColorAttachment(GL_RGBA32UI)); + UNSCOPED_INFO("R32UI target allocates"); + CHECK(mInteger.allocate(INT_W, INT_H, GL_R32UI, /*depth=*/true)); + UNSCOPED_INFO("RG16I attaches"); + CHECK(mInteger.addColorAttachment(GL_RG16I)); + UNSCOPED_INFO("RGBA32UI attaches"); + CHECK(mInteger.addColorAttachment(GL_RGBA32UI)); const GLenum formats[] = { GL_R32UI, GL_RG16I, GL_RGBA32UI }; for (U32 attachment = 0; attachment < 3; ++attachment) { - ensure_equals("attachment " + std::to_string(attachment) + " recorded", mInteger.getInternalFormat(attachment), + UNSCOPED_INFO("attachment " + std::to_string(attachment) + " recorded"); + REQUIRE(mInteger.getInternalFormat(attachment) == (U32)formats[attachment]); gGL.getTextureSlot(0)->bindManual(ALTextureSlot::TT_TEXTURE, mInteger.getTexture(attachment)); GLint format = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &format); - ensure_equals("attachment " + std::to_string(attachment) + " storage", (U32)format, (U32)formats[attachment]); + UNSCOPED_INFO("attachment " + std::to_string(attachment) + " storage"); + REQUIRE((U32)format == (U32)formats[attachment]); } gGL.getTextureSlot(0)->unbind(); mInteger.bindTarget(); - ensure_equals("complete", (U32)glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER), (U32)GL_FRAMEBUFFER_COMPLETE); + UNSCOPED_INFO("complete"); + CHECK((U32)glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == (U32)GL_FRAMEBUFFER_COMPLETE); gGL.setClearColor(LLColor4(7.f, 9.f, 11.f, 13.f)); mInteger.clear(); @@ -292,25 +306,34 @@ namespace tut { const std::string at = " at " + std::to_string(x) + "," + std::to_string(y); const size_t i = ((size_t)y * INT_W + x) * 4; - ensure_equals("R32UI drawn" + at, uints[i], 0x01000001u + y * 1000u + x); - ensure_equals("RG16I r drawn" + at, ints[i], -1 - (S32)x); - ensure_equals("RG16I g drawn" + at, ints[i + 1], (S32)y * 300 - 1000); - ensure_equals("RGBA32UI r drawn" + at, wide[i], x); - ensure_equals("RGBA32UI g drawn" + at, wide[i + 1], y); - ensure_equals("RGBA32UI b drawn" + at, wide[i + 2], 0xFFFFFFFFu - x); - ensure_equals("RGBA32UI a drawn" + at, wide[i + 3], 0x80000000u + y); + UNSCOPED_INFO("R32UI drawn" + at); + REQUIRE(uints[i] == 0x01000001u + y * 1000u + x); + UNSCOPED_INFO("RG16I r drawn" + at); + REQUIRE(ints[i] == -1 - (S32)x); + UNSCOPED_INFO("RG16I g drawn" + at); + REQUIRE(ints[i + 1] == (S32)y * 300 - 1000); + UNSCOPED_INFO("RGBA32UI r drawn" + at); + REQUIRE(wide[i] == x); + UNSCOPED_INFO("RGBA32UI g drawn" + at); + REQUIRE(wide[i + 1] == y); + UNSCOPED_INFO("RGBA32UI b drawn" + at); + REQUIRE(wide[i + 2] == 0xFFFFFFFFu - x); + UNSCOPED_INFO("RGBA32UI a drawn" + at); + REQUIRE(wide[i + 3] == 0x80000000u + y); } } - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + UNSCOPED_INFO("no GL error"); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } // A target of both kinds: clear takes the float attachment to the clear colour and the integer one to its integers, // and with the draw buffers narrowed to the first it clears that one alone, as it would a target of one kind. - template<> template<> - void llrendertarget_object_t::test<3>() + TEST_CASE_METHOD(llrendertarget_data, "llrendertarget_gl: a mixed float and integer target clears each attachment as its kind", "[llrendertarget_gl]") { - ensure("RGBA8 target allocates", mMixed.allocate(INT_W, INT_H, GL_RGBA8)); - ensure("R32UI attaches", mMixed.addColorAttachment(GL_R32UI)); + UNSCOPED_INFO("RGBA8 target allocates"); + CHECK(mMixed.allocate(INT_W, INT_H, GL_RGBA8)); + UNSCOPED_INFO("R32UI attaches"); + CHECK(mMixed.addColorAttachment(GL_R32UI)); mMixed.bindTarget(); gGL.setClearColor(LLColor4(1.f, 0.f, 1.f, 1.f)); @@ -329,22 +352,24 @@ namespace tut ensureEvery("R32UI not drawn to, so left", readInteger(1), { 1u, 0u, 0u, 1u }); mMixed.flush(); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + UNSCOPED_INFO("no GL error"); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } // A texture lent by setColorAttachment answers for its format as one the target allocated does, so an integer one // clears to its integers; and returning it forgets the format. - template<> template<> - void llrendertarget_object_t::test<4>() + TEST_CASE_METHOD(llrendertarget_data, "llrendertarget_gl: a texture lent by setColorAttachment has its format until returned", "[llrendertarget_gl]") { const std::vector zeros((size_t)INT_W * INT_H, 0u); mLentImage = new LLImageGL(INT_W, INT_H, 1, /*usemipmaps=*/false); mLentImage->setExplicitFormat(GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT); - ensure("R32UI image created", mLentImage->createGLTexture(0, reinterpret_cast(zeros.data()))); + UNSCOPED_INFO("R32UI image created"); + CHECK(mLentImage->createGLTexture(0, reinterpret_cast(zeros.data()))); LLRenderTarget::sUseFBO = true; mLent.setColorAttachment(mLentImage.get()); - ensure_equals("the lent texture's format", mLent.getInternalFormat(0), (U32)GL_R32UI); + UNSCOPED_INFO("the lent texture's format"); + CHECK(mLent.getInternalFormat(0) == (U32)GL_R32UI); mLent.bindTarget(); gGL.setClearColor(LLColor4(5.f, 6.f, 7.f, 8.f)); @@ -353,7 +378,9 @@ namespace tut mLent.flush(); mLent.releaseColorAttachment(); - ensure_equals("returned, its format forgotten", mLent.getInternalFormat(0), 0u); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + UNSCOPED_INFO("returned, its format forgotten"); + CHECK(mLent.getInternalFormat(0) == 0u); + UNSCOPED_INFO("no GL error"); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } } diff --git a/indra/llrender/tests/lluiimage_test.cpp b/indra/llrender/tests/lluiimage_test.cpp index d9d0ef772e..99ab849071 100644 --- a/indra/llrender/tests/lluiimage_test.cpp +++ b/indra/llrender/tests/lluiimage_test.cpp @@ -39,7 +39,7 @@ #include "../llvertexbuffer.h" #include "llimage.h" -#include "../test/lltut.h" +#include #include #include @@ -88,8 +88,6 @@ namespace ll_test }; } -namespace tut -{ namespace { struct lluiimage_data @@ -125,7 +123,8 @@ namespace // A recording's bounds are the image's size, from the origin. static void ensureExtent(const std::vector& verts, F32 width, F32 height) { - ensure("the recording holds vertices", !verts.empty()); + UNSCOPED_INFO("the recording holds vertices"); + REQUIRE_FALSE(verts.empty()); F32 min_x = verts[0].mV[VX]; F32 max_x = min_x; F32 min_y = verts[0].mV[VY]; @@ -137,55 +136,53 @@ namespace min_y = llmin(min_y, v.mV[VY]); max_y = llmax(max_y, v.mV[VY]); } - ensure("starts at the origin in x", min_x == 0.f); - ensure("starts at the origin in y", min_y == 0.f); - ensure("spans the width", max_x == width); - ensure("spans the height", max_y == height); + UNSCOPED_INFO("starts at the origin in x"); + REQUIRE(min_x == 0.f); + UNSCOPED_INFO("starts at the origin in y"); + REQUIRE(min_y == 0.f); + UNSCOPED_INFO("spans the width"); + REQUIRE(max_x == width); + UNSCOPED_INFO("spans the height"); + REQUIRE(max_y == height); } }; - typedef test_group lluiimage_test; - typedef lluiimage_test::object lluiimage_object; - tut::lluiimage_test lluiimage_testcase("LLUIImage"); -} // One recording serves every position the image is drawn at. A floater // being dragged is this test run at frame rate. - template<> template<> - void lluiimage_object::test<1>() + TEST_CASE_METHOD(lluiimage_data, "LLUIImage: one recording serves every position but not every size or colour", "[LLUIImage]") { LLPointer texture = makeTexture(4, 4); LLPointer image = new LLUIImage("probe", texture.get()); image->draw(10, 10, 8, 8); - ensure_equals("the first draw records", - ll_test::UIImageProbe::recordings(*image), (size_t)1); + UNSCOPED_INFO("the first draw records"); + CHECK(ll_test::UIImageProbe::recordings(*image) == (size_t)1); image->draw(100, 50, 8, 8); - ensure_equals("another position replays", - ll_test::UIImageProbe::recordings(*image), (size_t)1); + UNSCOPED_INFO("another position replays"); + CHECK(ll_test::UIImageProbe::recordings(*image) == (size_t)1); gGL.pushUIMatrix(); gGL.translateUI(30.f, 40.f, 0.f); image->draw(10, 10, 8, 8); gGL.popUIMatrix(); - ensure_equals("another UI offset replays", - ll_test::UIImageProbe::recordings(*image), (size_t)1); + UNSCOPED_INFO("another UI offset replays"); + CHECK(ll_test::UIImageProbe::recordings(*image) == (size_t)1); image->draw(10, 10, 16, 8); - ensure_equals("another size is another recording", - ll_test::UIImageProbe::recordings(*image), (size_t)2); + UNSCOPED_INFO("another size is another recording"); + CHECK(ll_test::UIImageProbe::recordings(*image) == (size_t)2); image->draw(10, 10, 8, 8, LLColor4::red); - ensure_equals("another colour is another recording", - ll_test::UIImageProbe::recordings(*image), (size_t)3); + UNSCOPED_INFO("another colour is another recording"); + CHECK(ll_test::UIImageProbe::recordings(*image) == (size_t)3); } // The recording holds the image's shape and nothing of where it was made: // drawn under a UI offset and away from the origin, its vertices still // span exactly [0, width] x [0, height]. - template<> template<> - void lluiimage_object::test<2>() + TEST_CASE_METHOD(lluiimage_data, "LLUIImage: a plain image's recording spans its size from the origin", "[LLUIImage]") { LLPointer texture = makeTexture(4, 4); LLPointer image = new LLUIImage("probe", texture.get()); @@ -196,14 +193,14 @@ namespace gGL.popUIMatrix(); const std::vector verts = ll_test::UIImageProbe::positions(*image); - ensure_equals("a plain image is one quad", verts.size(), (size_t)6); + UNSCOPED_INFO("a plain image is one quad"); + REQUIRE(verts.size() == (size_t)6); ensureExtent(verts, 8.f, 6.f); } // The nine-slice path works out its own placement from the UI offset, so // it is checked apart from the plain quad. - template<> template<> - void lluiimage_object::test<3>() + TEST_CASE_METHOD(lluiimage_data, "LLUIImage: a nine-slice image's recording spans its size from the origin", "[LLUIImage]") { LLPointer texture = makeTexture(16, 16); LLPointer image = new LLUIImage("probe", texture.get()); @@ -215,7 +212,8 @@ namespace gGL.popUIMatrix(); const std::vector verts = ll_test::UIImageProbe::positions(*image); - ensure_equals("nine slices are eighteen triangles", verts.size(), (size_t)54); + UNSCOPED_INFO("nine slices are eighteen triangles"); + REQUIRE(verts.size() == (size_t)54); ensureExtent(verts, 32.f, 24.f); } } diff --git a/indra/llrender/tests/lluitransform_test.cpp b/indra/llrender/tests/lluitransform_test.cpp index 7e0af297f1..fc12fe4bb1 100644 --- a/indra/llrender/tests/lluitransform_test.cpp +++ b/indra/llrender/tests/lluitransform_test.cpp @@ -27,10 +27,8 @@ #include "../llfontgl.h" #include "../llrender2dutils.h" -#include "../test/lltut.h" +#include -namespace tut -{ namespace { struct lluitransform_data @@ -61,30 +59,29 @@ namespace } }; - typedef test_group lluitransform_test; - typedef lluitransform_test::object lluitransform_object; - tut::lluitransform_test lluitransform_testgroup("lluitransform"); -} // With nothing pushed, a local rect is where it says it is. - template<> template<> - void lluitransform_object::test<1>() + TEST_CASE_METHOD(lluitransform_data, "lluitransform: with nothing pushed a local rect is where it says it is", "[lluitransform]") { const LLRect local(10, 40, 110, 20); const LLRect screen = LLRender2D::toScreen(local); - ensure_equals("untransformed, it is itself " + where(screen), where(screen), where(local)); + UNSCOPED_INFO("untransformed, it is itself " + where(screen)); + CHECK(where(screen) == where(local)); } // An offset moves it, which is all this ever did. - template<> template<> - void lluitransform_object::test<2>() + TEST_CASE_METHOD(lluitransform_data, "lluitransform: an offset moves a rect and keeps its size", "[lluitransform]") { LLFontGL::sCurOrigin.set(7, 13); const LLRect screen = LLRender2D::toScreen(LLRect(10, 40, 110, 20)); - ensure_equals("moved by the offset", screen.mLeft, 17); - ensure_equals("in both directions", screen.mBottom, 33); - ensure_equals("keeping its width", screen.getWidth(), 100); - ensure_equals("and its height", screen.getHeight(), 20); + UNSCOPED_INFO("moved by the offset"); + CHECK(screen.mLeft == 17); + UNSCOPED_INFO("in both directions"); + CHECK(screen.mBottom == 33); + UNSCOPED_INFO("keeping its width"); + CHECK(screen.getWidth() == 100); + UNSCOPED_INFO("and its height"); + CHECK(screen.getHeight() == 20); } // A scale grows it about the screen origin, and the offset is scaled with @@ -92,36 +89,40 @@ namespace // arithmetic the renderer does to every one of them. Clipping used to // apply the offset and not the scale, so a scissor sat where the drawing // would have been at a hundred per cent. - template<> template<> - void lluitransform_object::test<3>() + TEST_CASE_METHOD(lluitransform_data, "lluitransform: a scale grows a rect about the screen origin and scales the offset", "[lluitransform]") { LLFontGL::sCurOrigin.set(10, 20); LLFontGL::sCurScaleX = 2.f; LLFontGL::sCurScaleY = 2.f; const LLRect screen = LLRender2D::toScreen(LLRect(0, 50, 100, 0)); - ensure_equals("the near edge is the scaled offset", screen.mLeft, 20); - ensure_equals("and so is the bottom", screen.mBottom, 40); - ensure_equals("the far edge is scaled too", screen.mRight, 220); - ensure_equals("the rect is twice the size", screen.getWidth(), 200); - ensure_equals("in both directions", screen.getHeight(), 100); + UNSCOPED_INFO("the near edge is the scaled offset"); + CHECK(screen.mLeft == 20); + UNSCOPED_INFO("and so is the bottom"); + CHECK(screen.mBottom == 40); + UNSCOPED_INFO("the far edge is scaled too"); + CHECK(screen.mRight == 220); + UNSCOPED_INFO("the rect is twice the size"); + CHECK(screen.getWidth() == 200); + UNSCOPED_INFO("in both directions"); + CHECK(screen.getHeight() == 100); } // The two axes are independent, because the renderer's are. - template<> template<> - void lluitransform_object::test<4>() + TEST_CASE_METHOD(lluitransform_data, "lluitransform: the two axes scale independently", "[lluitransform]") { LLFontGL::sCurScaleX = 3.f; LLFontGL::sCurScaleY = 1.f; const LLRect screen = LLRender2D::toScreen(LLRect(0, 10, 10, 0)); - ensure_equals("across", screen.getWidth(), 30); - ensure_equals("down is left alone", screen.getHeight(), 10); + UNSCOPED_INFO("across"); + CHECK(screen.getWidth() == 30); + UNSCOPED_INFO("down is left alone"); + CHECK(screen.getHeight() == 10); } // A point and a rect agree, since the rect is two points. - template<> template<> - void lluitransform_object::test<5>() + TEST_CASE_METHOD(lluitransform_data, "lluitransform: a point and a rect's corner land in the same place", "[lluitransform]") { LLFontGL::sCurOrigin.set(4, 6); LLFontGL::sCurScaleX = 1.5f; @@ -130,19 +131,22 @@ namespace F32 x = 0.f; F32 y = 0.f; LLRender2D::toScreen(20.f, 30.f, x, y); - ensure_equals("across", (S32)ll_round(x), 36); - ensure_equals("down", (S32)ll_round(y), 90); + UNSCOPED_INFO("across"); + CHECK((S32)ll_round(x) == 36); + UNSCOPED_INFO("down"); + CHECK((S32)ll_round(y) == 90); const LLRect screen = LLRender2D::toScreen(LLRect(20, 30, 20, 30)); - ensure_equals("the rect's corner is the same point", screen.mLeft, 36); - ensure_equals("and the same on the other axis", screen.mBottom, 90); + UNSCOPED_INFO("the rect's corner is the same point"); + CHECK(screen.mLeft == 36); + UNSCOPED_INFO("and the same on the other axis"); + CHECK(screen.mBottom == 90); } // A scale keeps the origin it was pushed at: what is under it is drawn // larger where it already was, rather than being carried away from the // window's corner in proportion to how far along it had got. - template<> template<> - void lluitransform_object::test<6>() + TEST_CASE_METHOD(lluitransform_data, "lluitransform: a scale keeps the origin it was pushed at", "[lluitransform]") { LLRender2D::pushMatrix(); LLRender2D::translate(300.f, 200.f); @@ -151,20 +155,23 @@ namespace F32 x = 0.f; F32 y = 0.f; LLRender2D::toScreen(0.f, 0.f, x, y); - ensure_equals("the corner it was pushed at is where it was", ll_round(x), 300); - ensure_equals("on both axes", ll_round(y), 200); + UNSCOPED_INFO("the corner it was pushed at is where it was"); + CHECK(ll_round(x) == 300); + UNSCOPED_INFO("on both axes"); + CHECK(ll_round(y) == 200); LLRender2D::toScreen(50.f, 40.f, x, y); - ensure_equals("and what is on it is twice as far along", ll_round(x), 400); - ensure_equals("in both directions", ll_round(y), 280); + UNSCOPED_INFO("and what is on it is twice as far along"); + CHECK(ll_round(x) == 400); + UNSCOPED_INFO("in both directions"); + CHECK(ll_round(y) == 280); LLRender2D::popMatrix(); } // Taking the frame off takes the scale with it, and what it did to the // offset to keep the origin. - template<> template<> - void lluitransform_object::test<7>() + TEST_CASE_METHOD(lluitransform_data, "lluitransform: popMatrix takes off the frame's scale and what it did to the offset", "[lluitransform]") { LLRender2D::pushMatrix(); LLRender2D::translate(30.f, 40.f); @@ -175,9 +182,12 @@ namespace F32 x = 0.f; F32 y = 0.f; LLRender2D::toScreen(10.f, 10.f, x, y); - ensure_equals("the offset is the one the frame was pushed on", ll_round(x), 40); - ensure_equals("on both axes", ll_round(y), 50); - ensure_equals("and the scale is off with it", LLFontGL::sCurScaleX, 1.f); + UNSCOPED_INFO("the offset is the one the frame was pushed on"); + CHECK(ll_round(x) == 40); + UNSCOPED_INFO("on both axes"); + CHECK(ll_round(y) == 50); + UNSCOPED_INFO("and the scale is off with it"); + CHECK(LLFontGL::sCurScaleX == 1.f); LLRender2D::popMatrix(); } From bb3f752d2861a6b34136b92c63014557145b553e Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 13 Oct 2026 07:25:50 -0400 Subject: [PATCH 2/4] llrender's tests read as Catch2 The idiom pass over TM22's conversion, by seven agents on copies, merged back onto it. Names, messages and assertion strength only: every case and every assertion is where the conversion put it. - Names: 388 of 392 changed. Most were the first sentence of the comment above the test, cut off mid-phrase, and 85 had commas, which a Catch2 test spec reads as "or". - Messages: 2,040 to 576, with 66 CAPTUREs. Most of those removed restated their check ("no GL error" before a check for none, "font resolves" before a null check); most that remain say which pass, pixel or driver case a failure is in. - 12 asserting helpers are named require_*: require_ints, require_near, require_matrix_close, require_program and the like. - 105 CHECKs are REQUIREs: a program built, a texture or target made, or a size read, that the next lines use, where going on would draw with a failed GL object or end the process in an llassert. 128 REQUIREs that guarded nothing are CHECKs. - One FAIL's message, built from six numbers, is a CAPTURE of them. tut_parity.py holds all 26 files under --recounted, every case's count unchanged, with the conversion's seven sites reviewed as never running here. Old test bugs the agents found, left as they were: - checks that cannot fail: CHECK(true) in llfonttextcache, llfontfreetype and llfontregistry; checks after an if (!x) SKIP of the same x in alfontshaping, alfontcolrv1 and llfontfreetype; isValid() on a face getOrCreateFace returned, which is never invalid; llimagegl's mip width, which glTexStorage2D gives every level, and a binding read just after the test bound it; alfontcolrv1's force-mono check, which tests the test's own ternary; - reads with no guard, where a failure would crash rather than fail: getFontFreetype(), getFontFace(), templateFor(), getBitmapCache() and loaded faces dereferenced in llfontgl, llfontregistry, llfontfreetype, alfontshaping and alfontcolrv1; getFontFiles()[0] and mPhaseSlots[0] unsized; llimagegl's texel reader unbounded; llglslshader's uniform locations, storage binding points and texture units never checked for -1, so a missing uniform reads zero and every zero expected passes; - checks that test something else or less than they say: alfontshaping compares a codepoint count with a byte count; llfontbitmapcache reads the generation before init(), whose reset bumps it; llfontfreetype's address compare cannot tell a cache hit from a reused block; comments promise checks that are not there (alfontcolrv1, alfontshaping, llfontregistry, llfonttextcache, llfontfreetype, lluitransform's Y scale); - state a failure leaks: alfontcolrv1_gl's sForceMonochromeEmoji when a REQUIRE fails before it is put back; alinfiniteprojection's queries and alfontshaping's mm; - leftovers: alfontshaping's a_addr, set and never read; a case llfonttextcache has twice; alimageslot's fixture comment. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/tests/alfontcolrv1_gl_test.cpp | 42 +- indra/llrender/tests/alfontcolrv1_test.cpp | 230 ++----- .../llrender/tests/alfontshaping_gl_test.cpp | 32 +- indra/llrender/tests/alfontshaping_test.cpp | 326 +++------- indra/llrender/tests/algpubuffer_gl_test.cpp | 73 +-- indra/llrender/tests/alimageslot_gl_test.cpp | 109 +--- .../tests/alinfiniteprojection_gl_test.cpp | 53 +- .../tests/alinstanceddraw_gl_test.cpp | 3 - .../llrender/tests/alrenderstate_gl_test.cpp | 23 +- .../llrender/tests/altessellation_gl_test.cpp | 30 +- .../tests/alwindowsdlheadless_test.cpp | 11 +- .../tests/llfontbitmapcache_gl_test.cpp | 115 +--- .../llrender/tests/llfontbitmapcache_test.cpp | 24 +- .../llrender/tests/llfontfreetype_gl_test.cpp | 85 +-- indra/llrender/tests/llfontfreetype_test.cpp | 250 +++----- indra/llrender/tests/llfontgl_test.cpp | 150 ++--- .../llrender/tests/llfontregistry_gl_test.cpp | 82 +-- indra/llrender/tests/llfontregistry_test.cpp | 571 +++--------------- indra/llrender/tests/llfonttextcache_test.cpp | 35 +- indra/llrender/tests/llglslshader_gl_test.cpp | 458 ++++++-------- indra/llrender/tests/llimagegl_test.cpp | 217 ++----- .../tests/llrender2dutils_gl_test.cpp | 42 +- indra/llrender/tests/llrendermatrix_test.cpp | 48 +- .../llrender/tests/llrendertarget_gl_test.cpp | 82 +-- indra/llrender/tests/lluiimage_test.cpp | 11 +- indra/llrender/tests/lluitransform_test.cpp | 65 +- 26 files changed, 816 insertions(+), 2351 deletions(-) diff --git a/indra/llrender/tests/alfontcolrv1_gl_test.cpp b/indra/llrender/tests/alfontcolrv1_gl_test.cpp index 29caa226a4..510858dd25 100644 --- a/indra/llrender/tests/alfontcolrv1_gl_test.cpp +++ b/indra/llrender/tests/alfontcolrv1_gl_test.cpp @@ -72,29 +72,20 @@ namespace if (!fileExists(path)) SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFtHead(path); - UNSCOPED_INFO("Noto-COLRv1 loaded as head"); REQUIRE(ft.notNull()); LLFontGlyphInfo* gi = ft->getGlyphInfo(0x1F525, EFontGlyphType::Color); - UNSCOPED_INFO("getGlyphInfo returned an entry for fire emoji"); REQUIRE(gi != nullptr); - UNSCOPED_INFO("delivered glyph type is Color (COLRv1 raster)"); CHECK((S32)gi->mGlyphType == (S32)EFontGlyphType::Color); - UNSCOPED_INFO("glyph has positive width"); CHECK(gi->mWidth > 0); - UNSCOPED_INFO("glyph has positive height"); CHECK(gi->mHeight > 0); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - UNSCOPED_INFO("bitmap cache present"); REQUIRE(cache != nullptr); - UNSCOPED_INFO("Color atlas allocated at least one sheet"); CHECK(cache->getNumBitmaps(EFontGlyphType::Color) >= 1u); LLImageGL* page = cache->getImageGL(EFontGlyphType::Color, 0); - UNSCOPED_INFO("Color sheet 0 LLImageGL present"); REQUIRE(page != nullptr); - UNSCOPED_INFO("Color sheet 0 has a live GL texture name"); CHECK(page->getTexName() != 0); } @@ -108,22 +99,17 @@ namespace if (!fileExists(path)) SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFtHead(path); - UNSCOPED_INFO("Noto-COLRv1 loaded as head"); REQUIRE(ft.notNull()); const llwchar fire = 0x1F525; const llwchar heart = 0x2764; const llwchar rocket = 0x1F680; - UNSCOPED_INFO("fire glyph allocated"); REQUIRE(ft->getGlyphInfo(fire, EFontGlyphType::Color) != nullptr); - UNSCOPED_INFO("heart glyph allocated"); REQUIRE(ft->getGlyphInfo(heart, EFontGlyphType::Color) != nullptr); - UNSCOPED_INFO("rocket glyph allocated"); REQUIRE(ft->getGlyphInfo(rocket, EFontGlyphType::Color) != nullptr); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - UNSCOPED_INFO("three Color glyphs share a single Color atlas sheet"); CHECK(cache->getNumBitmaps(EFontGlyphType::Color) == 1u); } @@ -137,17 +123,13 @@ namespace if (!fileExists(path)) SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFtHead(path); - UNSCOPED_INFO("Noto-COLRv1 loaded as head"); REQUIRE(ft.notNull()); LLFontGlyphInfo* first = ft->getGlyphInfo(0x1F525, EFontGlyphType::Color); LLFontGlyphInfo* second = ft->getGlyphInfo(0x1F525, EFontGlyphType::Color); - UNSCOPED_INFO("first lookup non-null"); REQUIRE(first != nullptr); - UNSCOPED_INFO("second lookup non-null"); REQUIRE(second != nullptr); - UNSCOPED_INFO("repeat lookup returns same LLFontGlyphInfo pointer"); - REQUIRE(first == second); + CHECK(first == second); } // Color atlas generation counter advances on the first emoji @@ -161,18 +143,15 @@ namespace if (!fileExists(path)) SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFtHead(path); - UNSCOPED_INFO("Noto-COLRv1 loaded as head"); REQUIRE(ft.notNull()); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); const S32 gen_before = cache->getCacheGeneration(); LLFontGlyphInfo* gi = ft->getGlyphInfo(0x1F525, EFontGlyphType::Color); - UNSCOPED_INFO("glyph allocated"); REQUIRE(gi != nullptr); const S32 gen_after = cache->getCacheGeneration(); - UNSCOPED_INFO("cache generation advanced after Color rasterize"); CHECK(gen_after > gen_before); } @@ -201,14 +180,10 @@ namespace ALFontColrV1Painter painter; ALFontColrV1Painter::Result r1, r2; const LLColor4U fg(255, 255, 255, 255); - UNSCOPED_INFO("first paintGlyph succeeded"); REQUIRE(painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r1)); const S32 w1 = r1.mWidth, h1 = r1.mHeight; - UNSCOPED_INFO("second paintGlyph (same glyph) succeeded"); REQUIRE(painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r2)); - UNSCOPED_INFO("repeat paint width matches"); CHECK(r2.mWidth == w1); - UNSCOPED_INFO("repeat paint height matches"); CHECK(r2.mHeight == h1); } @@ -224,28 +199,19 @@ namespace if (!fileExists(path)) SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFtHead(path); - UNSCOPED_INFO("Noto-COLRv1 loaded as head"); REQUIRE(ft.notNull()); LLFontGlyphInfo* gi = ft->getGlyphInfo(0x1F525, EFontGlyphType::Grayscale); - UNSCOPED_INFO("Grayscale lookup returns an entry"); REQUIRE(gi != nullptr); - UNSCOPED_INFO("delivered glyph type is Grayscale (LA atlas)"); CHECK((S32)gi->mGlyphType == (S32)EFontGlyphType::Grayscale); - UNSCOPED_INFO("Grayscale glyph has positive width"); CHECK(gi->mWidth > 0); - UNSCOPED_INFO("Grayscale glyph has positive height"); CHECK(gi->mHeight > 0); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - UNSCOPED_INFO("bitmap cache present"); REQUIRE(cache != nullptr); - UNSCOPED_INFO("Grayscale atlas allocated at least one sheet"); CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); LLImageGL* page = cache->getImageGL(EFontGlyphType::Grayscale, 0); - UNSCOPED_INFO("Grayscale sheet 0 LLImageGL present"); REQUIRE(page != nullptr); - UNSCOPED_INFO("Grayscale sheet 0 has a live GL texture name"); CHECK(page->getTexName() != 0); } @@ -265,7 +231,6 @@ namespace LLFontGL::sForceMonochromeEmoji = true; LLPointer ft = loadFtHead(path); - UNSCOPED_INFO("Noto-COLRv1 loaded as head"); REQUIRE(ft.notNull()); // Simulate the render-call selector at llfontgl.cpp:560 with @@ -275,13 +240,10 @@ namespace ? EFontGlyphType::Grayscale : EFontGlyphType::Color; UNSCOPED_INFO("force-mono toggle downgrades Color to Grayscale at " "the request layer"); - CHECK((S32)requested == - (S32)EFontGlyphType::Grayscale); + CHECK((S32)requested == (S32)EFontGlyphType::Grayscale); LLFontGlyphInfo* gi = ft->getGlyphInfo(0x1F525, requested); - UNSCOPED_INFO("Grayscale lookup under force-mono returns an entry"); REQUIRE(gi != nullptr); - UNSCOPED_INFO("Grayscale path landed in Grayscale atlas"); CHECK((S32)gi->mGlyphType == (S32)EFontGlyphType::Grayscale); LLFontGL::sForceMonochromeEmoji = saved; diff --git a/indra/llrender/tests/alfontcolrv1_test.cpp b/indra/llrender/tests/alfontcolrv1_test.cpp index 4b830a04e5..191907b650 100644 --- a/indra/llrender/tests/alfontcolrv1_test.cpp +++ b/indra/llrender/tests/alfontcolrv1_test.cpp @@ -65,10 +65,9 @@ namespace { using namespace ll_test; - // Each test_group<>::object spins up a fresh fixture for every test, - // so init/cleanup the font manager once per test rather than once per - // process. Cheaper than tracking init state and matches the test- - // isolation model TUT expects. + // Each case gets a fresh fixture, so init/cleanup the font manager + // once per test rather than once per process. Cheaper than tracking + // init state and keeps each case isolated from the others. struct alfontcolrv1_data { alfontcolrv1_data() @@ -90,13 +89,10 @@ namespace ALFontColrV1Painter painter; ALFontColrV1Painter::Result result; const LLColor4U fg(255, 255, 255, 255); - UNSCOPED_INFO("paintGlyph(null hb_font) returns false"); - REQUIRE_FALSE(painter.paintGlyph(nullptr, /*glyph_index=*/0, + CHECK_FALSE(painter.paintGlyph(nullptr, /*glyph_index=*/0, /*point_size=*/14.f, fg, /*palette_index=*/0, result)); - UNSCOPED_INFO("Result.mBitmap stays null on failure"); - REQUIRE(result.mBitmap == nullptr); - UNSCOPED_INFO("Result.mWidth stays zero on failure"); + CHECK(result.mBitmap == nullptr); CHECK(result.mWidth == 0); } @@ -112,14 +108,9 @@ namespace } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - { - INFO("Noto-COLRv1 loaded"); - REQUIRE(face.notNull()); - CHECK(face->isValid()); - } - UNSCOPED_INFO("Noto-COLRv1 hasColrV1 == true"); + REQUIRE(face.notNull()); + CHECK(face->isValid()); CHECK(face->hasColrV1()); - UNSCOPED_INFO("Noto-COLRv1 hasColor == true (general flag)"); CHECK(face->hasColor()); } @@ -135,12 +126,8 @@ namespace } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - { - INFO("Inter loaded"); - REQUIRE(face.notNull()); - CHECK(face->isValid()); - } - UNSCOPED_INFO("Inter hasColrV1 == false"); + REQUIRE(face.notNull()); + CHECK(face->isValid()); CHECK_FALSE(face->hasColrV1()); } @@ -158,43 +145,30 @@ namespace } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - { - INFO("face loaded"); - REQUIRE(face.notNull()); - CHECK(face->isValid()); - } - UNSCOPED_INFO("face has COLRv1"); + REQUIRE(face.notNull()); + CHECK(face->isValid()); CHECK(face->hasColrV1()); hb_font_t* hb_font = face->getHbFont(); - UNSCOPED_INFO("hb_font available"); REQUIRE(hb_font != nullptr); // Resolve the fire emoji's glyph index via the cmap. Avoids hardcoding // a glyph_id that could shift across font versions. const hb_codepoint_t fire_cp = 0x1F525; hb_codepoint_t fire_gid = 0; - { - INFO("cmap maps U+1F525 to a glyph"); - CHECK(hb_font_get_nominal_glyph(hb_font, fire_cp, &fire_gid)); - CHECK(fire_gid != 0); - } + CHECK(hb_font_get_nominal_glyph(hb_font, fire_cp, &fire_gid)); + CHECK(fire_gid != 0); ALFontColrV1Painter painter; ALFontColrV1Painter::Result result; const LLColor4U fg(255, 255, 255, 255); const bool ok = painter.paintGlyph(hb_font, fire_gid, /*point_size=*/14.f, fg, /*palette_index=*/0, result); - UNSCOPED_INFO("paintGlyph succeeded"); CHECK(ok); - { - INFO("painter produced bitmap"); - REQUIRE(result.mBitmap != nullptr); - CHECK(result.mWidth > 0); - CHECK(result.mHeight > 0); - CHECK(result.mPitch != 0); - } - UNSCOPED_INFO("painter drew non-zero alpha somewhere"); + REQUIRE(result.mBitmap != nullptr); + CHECK(result.mWidth > 0); + CHECK(result.mHeight > 0); + CHECK(result.mPitch != 0); CHECK(anyAlpha(result.mBitmap, result.mWidth, result.mHeight, result.mPitch)); } @@ -211,17 +185,12 @@ namespace } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - { - INFO("face loaded"); - REQUIRE(face.notNull()); - CHECK(face->isValid()); - } + REQUIRE(face.notNull()); + CHECK(face->isValid()); // ZWJ should resolve to a real glyph (cmap entry present). - UNSCOPED_INFO("Noto cmap has ZWJ glyph"); CHECK(face->getCharGlyphIndex(0x200D) != 0u); // VS-16 should NOT — shape_sub_run keys VS-16 stripping on this. - UNSCOPED_INFO("Noto cmap has no VS-16 glyph"); CHECK(face->getCharGlyphIndex(0xFE0F) == 0u); } @@ -239,13 +208,9 @@ namespace } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - { - INFO("face loaded"); - REQUIRE(face.notNull()); - CHECK(face->isValid()); - } + REQUIRE(face.notNull()); + CHECK(face->isValid()); hb_font_t* hb_font = face->getHbFont(); - UNSCOPED_INFO("hb_font available"); REQUIRE(hb_font != nullptr); ALFontColrV1Painter painter; @@ -257,11 +222,8 @@ namespace const bool ok = painter.paintGlyph(hb_font, /*glyph_index=*/0xFFFFFFFE, 14.f, fg, 0, result); - UNSCOPED_INFO("paintGlyph on out-of-range glyph index fails cleanly"); CHECK_FALSE(ok); - UNSCOPED_INFO("result.mBitmap stays null on out-of-range"); - REQUIRE(result.mBitmap == nullptr); - UNSCOPED_INFO("result.mWidth zero on out-of-range"); + CHECK(result.mBitmap == nullptr); CHECK(result.mWidth == 0); } @@ -279,11 +241,8 @@ namespace } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - { - INFO("face loaded"); - REQUIRE(face.notNull()); - CHECK(face->isValid()); - } + REQUIRE(face.notNull()); + CHECK(face->isValid()); hb_font_t* hb_font = face->getHbFont(); // Two distinct emoji: fire (U+1F525) and red heart (U+2764). @@ -302,23 +261,15 @@ namespace ALFontColrV1Painter painter; ALFontColrV1Painter::Result r1, r2; - UNSCOPED_INFO("first paintGlyph (fire) succeeded"); REQUIRE(painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r1)); - { - INFO("first painter produced bitmap"); - REQUIRE(r1.mBitmap != nullptr); - CHECK(r1.mWidth > 0); - CHECK(r1.mHeight > 0); - } + REQUIRE(r1.mBitmap != nullptr); + CHECK(r1.mWidth > 0); + CHECK(r1.mHeight > 0); - UNSCOPED_INFO("second paintGlyph (heart) succeeded on same painter"); REQUIRE(painter.paintGlyph(hb_font, heart, 14.f, fg, 0, r2)); - { - INFO("second painter produced bitmap"); - REQUIRE(r2.mBitmap != nullptr); - CHECK(r2.mWidth > 0); - CHECK(r2.mHeight > 0); - } + REQUIRE(r2.mBitmap != nullptr); + CHECK(r2.mWidth > 0); + CHECK(r2.mHeight > 0); } // 2-em-square surface fallback (88585ff86d): a glyph with no @@ -341,11 +292,8 @@ namespace } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - { - INFO("face loaded"); - REQUIRE(face.notNull()); - CHECK(face->isValid()); - } + REQUIRE(face.notNull()); + CHECK(face->isValid()); hb_font_t* hb_font = face->getHbFont(); hb_codepoint_t fire = 0; @@ -357,7 +305,6 @@ namespace ALFontColrV1Painter painter; ALFontColrV1Painter::Result result; const LLColor4U fg(255, 255, 255, 255); - UNSCOPED_INFO("paintGlyph succeeded"); REQUIRE(painter.paintGlyph(hb_font, fire, point_size, fg, 0, result)); // Surface dimensions are on the order of point_size — the // ClipBox / FT-extents path produces a bitmap whose width and @@ -366,16 +313,10 @@ namespace // the lower bound; a regression that ballooned to 2× the // 2-em square (32×32 fallback for 14pt) would fail the upper // bound (~3× point_size). - { - INFO("surface width is in a reasonable range for 14pt"); - CHECK(result.mWidth >= 4); - CHECK(result.mWidth <= (S32)(point_size * 4.f)); - } - { - INFO("surface height is in a reasonable range for 14pt"); - CHECK(result.mHeight >= 4); - CHECK(result.mHeight <= (S32)(point_size * 4.f)); - } + CHECK(result.mWidth >= 4); + CHECK(result.mWidth <= (S32)(point_size * 4.f)); + CHECK(result.mHeight >= 4); + CHECK(result.mHeight <= (S32)(point_size * 4.f)); } // (Test 9 removed: pinned the mForegroundOnly flag on the painter @@ -407,14 +348,10 @@ namespace // them away. constexpr F32 ps = 32.f; const LLColor4U fg(255, 255, 255, 255); - UNSCOPED_INFO("paintGlyph succeeded"); REQUIRE(painter.paintGlyph(hb_font, fire, ps, fg, 0, result)); - { - INFO("painter produced bitmap"); - REQUIRE(result.mBitmap != nullptr); - CHECK(result.mWidth > 0); - CHECK(result.mHeight > 0); - } + REQUIRE(result.mBitmap != nullptr); + CHECK(result.mWidth > 0); + CHECK(result.mHeight > 0); std::set distinct; // packed BGR (alpha excluded) for (S32 y = 0; y < result.mHeight; ++y) @@ -432,8 +369,7 @@ namespace } // Fire emoji has at minimum a yellow body + orange gradient + // red highlights; >= 3 distinct triples is a conservative bound. - UNSCOPED_INFO("fire emoji produces >= 3 distinct RGB triples (gradient)"); - REQUIRE(distinct.size() >= 3); + CHECK(distinct.size() >= 3); } // palette_index argument propagates to hb_font_paint_glyph: paint @@ -459,17 +395,13 @@ namespace ALFontColrV1Painter painter_a, painter_b; ALFontColrV1Painter::Result r0, r1; - UNSCOPED_INFO("palette=0 paintGlyph succeeded"); REQUIRE(painter_a.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/0, r0)); - UNSCOPED_INFO("palette=1 paintGlyph succeeded"); REQUIRE(painter_b.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/1, r1)); - { - INFO("both produced bitmaps"); - REQUIRE(r0.mBitmap); - REQUIRE(r1.mBitmap); - CHECK(r0.mWidth == r1.mWidth); - CHECK(r0.mHeight == r1.mHeight); - } + REQUIRE(r0.mBitmap); + REQUIRE(r1.mBitmap); + // The comparison below walks r1 at r0's size. + REQUIRE(r0.mWidth == r1.mWidth); + REQUIRE(r0.mHeight == r1.mHeight); // Compare bytes — if the font has only one palette, hb resolves // both calls against palette 0 and the bitmaps match. Skip in @@ -493,7 +425,6 @@ namespace if (!any_diff) SKIP("Noto-COLRv1 ships only one CPAL palette"); - UNSCOPED_INFO("palette swap changed at least one painted pixel"); CHECK(any_diff); } @@ -517,20 +448,13 @@ namespace ALFontColrV1Painter painter; ALFontColrV1Painter::Result result; const LLColor4U fg(255, 255, 255, 255); - UNSCOPED_INFO("paintGlyph(format=Gray) succeeded"); REQUIRE(painter.paintGlyph(hb_font, fire, /*point_size=*/14.f, fg, /*palette=*/0, ALFontColrV1Painter::OutputFormat::Gray, result)); - UNSCOPED_INFO("Result.mFormat == Gray"); - CHECK((S32)result.mFormat == - (S32)ALFontColrV1Painter::OutputFormat::Gray); - { - INFO("Gray output has positive dims"); - CHECK(result.mWidth > 0); - CHECK(result.mHeight > 0); - } - UNSCOPED_INFO("Gray pitch is width (tightly packed single channel)"); + CHECK((S32)result.mFormat == (S32)ALFontColrV1Painter::OutputFormat::Gray); + CHECK(result.mWidth > 0); + CHECK(result.mHeight > 0); CHECK(result.mPitch == result.mWidth); bool any_nonzero = false; @@ -542,7 +466,6 @@ namespace if (row[x] != 0) { any_nonzero = true; break; } } } - UNSCOPED_INFO("Gray output has at least one non-zero coverage pixel"); CHECK(any_nonzero); } @@ -570,7 +493,6 @@ namespace ALFontColrV1Painter painter; ALFontColrV1Painter::Result rgba_r, gray_r; const LLColor4U fg(255, 255, 255, 255); - UNSCOPED_INFO("BGRA paint succeeded"); REQUIRE(painter.paintGlyph(hb_font, fire, 14.f, fg, 0, rgba_r)); // Stash the BGRA bytes — the next paintGlyph call invalidates // mBitmap (it points back into mStaging which we overwrite below). @@ -579,14 +501,12 @@ namespace const S32 bgra_w = rgba_r.mWidth, bgra_h = rgba_r.mHeight; const S32 bgra_pitch = rgba_r.mPitch; - UNSCOPED_INFO("Gray paint succeeded"); REQUIRE(painter.paintGlyph(hb_font, fire, 14.f, fg, 0, ALFontColrV1Painter::OutputFormat::Gray, gray_r)); - UNSCOPED_INFO("Gray and BGRA produce same width"); - CHECK(gray_r.mWidth == bgra_w); - UNSCOPED_INFO("Gray and BGRA produce same height"); - CHECK(gray_r.mHeight == bgra_h); + // The comparison below walks the Gray bitmap at the BGRA size. + REQUIRE(gray_r.mWidth == bgra_w); + REQUIRE(gray_r.mHeight == bgra_h); S32 mismatches = 0; for (S32 y = 0; y < bgra_h; ++y) @@ -609,7 +529,6 @@ namespace ++mismatches; } } - UNSCOPED_INFO("every Gray byte matches the hybrid fold formula"); CHECK(mismatches == 0); } @@ -669,8 +588,7 @@ namespace if (r == 0 && g == 0 && b == 0 && a > 200) { const U32 expected_floor = (a * 38u) >> 8; - UNSCOPED_INFO("Gray byte at saturated-black palette pixel " - "≥ alpha*floor (no crush to zero)"); + CAPTURE(cp, x, y); REQUIRE((U32)gray_row[x] + 1u >= expected_floor); return; } @@ -700,19 +618,14 @@ namespace ALFontColrV1Painter painter_a, painter_b; ALFontColrV1Painter::Result r_zero, r_huge; const LLColor4U fg(255, 255, 255, 255); - UNSCOPED_INFO("palette=0 paint succeeded"); REQUIRE(painter_a.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/0u, r_zero)); - UNSCOPED_INFO("palette=99 paint succeeded (clamps to 0)"); REQUIRE(painter_b.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/99u, r_huge)); - UNSCOPED_INFO("clamped result has same width"); CHECK(r_huge.mWidth == r_zero.mWidth); - UNSCOPED_INFO("clamped result has same height"); - CHECK(r_huge.mHeight == r_zero.mHeight); - UNSCOPED_INFO("clamped result has same pitch"); - CHECK(r_huge.mPitch == r_zero.mPitch); + // The memcmp below reads r_huge for r_zero's pitch times height. + REQUIRE(r_huge.mHeight == r_zero.mHeight); + REQUIRE(r_huge.mPitch == r_zero.mPitch); const size_t bytes = (size_t)r_zero.mPitch * r_zero.mHeight; - UNSCOPED_INFO("clamped result is byte-identical to palette=0"); CHECK(std::memcmp(r_zero.mBitmap, r_huge.mBitmap, bytes) == 0); } @@ -733,20 +646,15 @@ namespace // mutate its ppem without affecting the face cache shared with // other tests in this group. hb_font_t* shared = face->getHbFont(); - UNSCOPED_INFO("shared hb_font available"); REQUIRE(shared != nullptr); hb_face_t* hb_face = hb_font_get_face(shared); hb_font_t* hb_font = hb_font_create(hb_face); - UNSCOPED_INFO("derived hb_font allocated"); REQUIRE(hb_font != nullptr); hb_font_set_ppem(hb_font, 0, 0); unsigned px = 1, py = 1; hb_font_get_ppem(hb_font, &px, &py); - { - INFO("ppem really is unset on derived font"); - CHECK(px == 0); - CHECK(py == 0); - } + CHECK(px == 0); + CHECK(py == 0); // Pick a glyph by index — without a scale set, the cmap query // through hb_font_get_nominal_glyph may not work as expected, so @@ -776,11 +684,8 @@ namespace // failure mode we're protecting against. if (ok) { - { - INFO("fallback path produced a sized bitmap"); - CHECK(result.mWidth > 0); - CHECK(result.mHeight > 0); - } + CHECK(result.mWidth > 0); + CHECK(result.mHeight > 0); } hb_font_destroy(hb_font); } @@ -807,23 +712,19 @@ namespace ALFontColrV1Painter painter; ALFontColrV1Painter::Result r; const LLColor4U fg(255, 255, 255, 255); - UNSCOPED_INFO("paintGlyph succeeded"); REQUIRE(painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r)); - { - INFO("bitmap valid"); - REQUIRE(r.mBitmap); - CHECK(r.mWidth >= 4); - CHECK(r.mHeight >= 4); - } + REQUIRE(r.mBitmap); + // The loops below read row mHeight-1 and column mWidth-1. + REQUIRE(r.mWidth >= 4); + REQUIRE(r.mHeight >= 4); // First row, last row. for (S32 x = 0; x < r.mWidth; ++x) { const U8* top = r.mBitmap + x*4; const U8* bottom = r.mBitmap + (ptrdiff_t)(r.mHeight-1)*r.mPitch + x*4; - UNSCOPED_INFO("top row alpha is zero"); + CAPTURE(x); REQUIRE((S32)top[3] == 0); - UNSCOPED_INFO("bottom row alpha is zero"); REQUIRE((S32)bottom[3] == 0); } // First column, last column. @@ -831,9 +732,8 @@ namespace { const U8* left = r.mBitmap + (ptrdiff_t)y*r.mPitch + 0*4; const U8* right = r.mBitmap + (ptrdiff_t)y*r.mPitch + (r.mWidth-1)*4; - UNSCOPED_INFO("left col alpha is zero"); + CAPTURE(y); REQUIRE((S32)left[3] == 0); - UNSCOPED_INFO("right col alpha is zero"); REQUIRE((S32)right[3] == 0); } } diff --git a/indra/llrender/tests/alfontshaping_gl_test.cpp b/indra/llrender/tests/alfontshaping_gl_test.cpp index c6691d199d..379af02f3b 100644 --- a/indra/llrender/tests/alfontshaping_gl_test.cpp +++ b/indra/llrender/tests/alfontshaping_gl_test.cpp @@ -47,7 +47,7 @@ namespace // GL-backed group: monospace shaping ends up rendering glyphs through // getGlyphInfoByIndex → renderAndCreateGlyph → atlas → gGL.bind on - // the test 3 (kerning) path. Wrapped in a separate fixture that + // the kerning path (the GPOS case below). Wrapped in a separate fixture that // pulls in the headless GL context so the rasterizer can // satisfy the bind. struct alfontshaping_gl_data @@ -69,36 +69,25 @@ namespace if (!fileExists(path)) SKIP("DejaVuSansMono.woff2 not present"); LLPointer ft = loadFtHead(path); - UNSCOPED_INFO("DejaVuSansMono loaded"); REQUIRE(ft.notNull()); - UNSCOPED_INFO("DejaVuSansMono is fixed-width"); CHECK(ft->isFixedWidth()); - UNSCOPED_INFO("monospace ligatures default off"); CHECK_FALSE(ft->getAllowMonospaceLigatures()); const Text s = text('A','B'); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - UNSCOPED_INFO("AB produces 2 glyphs through HB strict-mono path"); REQUIRE(out.size() == 2u); - { - INFO("each shaped glyph has positive advance"); - CHECK(out[0].x_advance > 0.f); - CHECK(out[1].x_advance > 0.f); - } - UNSCOPED_INFO("strict-mono: A and B advances are equal"); + CHECK(out[0].x_advance > 0.f); + CHECK(out[1].x_advance > 0.f); CHECK(out[0].x_advance == out[1].x_advance); // HB output for monospace ASCII matches FT mXAdvance bit-exact // under the strict feature plan (verified by the long-line - // probe in test 20). - UNSCOPED_INFO("strict-mono glyph[0] advance == ft->getXAdvance('A')"); + // cell-alignment probe in alfontshaping_test.cpp). CHECK(out[0].x_advance == ft->getXAdvance(L'A')); // Strict-mono path also forces zero positioning offsets, since // monospace ASCII triggers no GPOS adjustments under the // feature plan (kern, mark, mkmk all suppressed for ASCII). - UNSCOPED_INFO("strict-mono glyph[0] x_offset is zero"); CHECK(out[0].x_offset == 0.f); - UNSCOPED_INFO("strict-mono glyph[0] y_offset is zero"); CHECK(out[0].y_offset == 0.f); } @@ -112,23 +101,16 @@ namespace if (!fileExists(path)) SKIP("DejaVuSansMono.woff2 not present"); LLPointer ft = loadFtHead(path); - UNSCOPED_INFO("DejaVuSansMono loaded"); REQUIRE(ft.notNull()); ft->setAllowMonospaceLigatures(true); - UNSCOPED_INFO("ligatures-on toggle applied"); CHECK(ft->getAllowMonospaceLigatures()); const Text s = text('A','V'); // AV is a classic kerned pair std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - UNSCOPED_INFO("AV produces 2 glyphs through HB"); REQUIRE(out.size() == 2u); - { - INFO("each shaped glyph has positive advance"); - CHECK(out[0].x_advance > 0.f); - CHECK(out[1].x_advance > 0.f); - } - UNSCOPED_INFO("HB-monospace-with-ligatures: A and V advances are equal"); + CHECK(out[0].x_advance > 0.f); + CHECK(out[1].x_advance > 0.f); CHECK(out[0].x_advance == out[1].x_advance); } @@ -145,7 +127,6 @@ namespace if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFtHead(path); - UNSCOPED_INFO("DejaVuSans loaded"); REQUIRE(ft.notNull()); // Try a handful of classic kerned pairs; pick the first that @@ -181,7 +162,6 @@ namespace if (!found_kerned) SKIP("DejaVuSans lacks a measurable Latin kern pair"); - UNSCOPED_INFO("at least one Latin kern pair fired through HB GPOS"); CHECK(found_kerned); } } diff --git a/indra/llrender/tests/alfontshaping_test.cpp b/indra/llrender/tests/alfontshaping_test.cpp index b937526501..7d7a336870 100644 --- a/indra/llrender/tests/alfontshaping_test.cpp +++ b/indra/llrender/tests/alfontshaping_test.cpp @@ -93,27 +93,26 @@ namespace const Text s = text('a','b','c'); ALFontShaping::shapeRun(/*root_face=*/nullptr, s, 0, s.size(), out); - UNSCOPED_INFO("null root face -> empty output"); - REQUIRE(out.empty()); + UNSCOPED_INFO("null root face"); + CHECK(out.empty()); const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("DejaVuSans loaded"); REQUIRE(ft.notNull()); ALFontShaping::shapeRun(ft, s, /*begin=*/2, /*end=*/2, out); - UNSCOPED_INFO("begin == end -> empty output"); - REQUIRE(out.empty()); + UNSCOPED_INFO("begin == end"); + CHECK(out.empty()); ALFontShaping::shapeRun(ft, s, /*begin=*/3, /*end=*/2, out); - UNSCOPED_INFO("begin > end -> empty output"); - REQUIRE(out.empty()); + UNSCOPED_INFO("begin > end"); + CHECK(out.empty()); ALFontShaping::shapeRun(ft, s, /*begin=*/0, /*end=*/99, out); - UNSCOPED_INFO("end > size -> empty output"); - REQUIRE(out.empty()); + UNSCOPED_INFO("end > size"); + CHECK(out.empty()); } // ASCII through a proportional Latin font: 1 glyph per codepoint, @@ -134,32 +133,24 @@ namespace if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("DejaVuSans loaded"); REQUIRE(ft.notNull()); const Text s = text('a','b','c'); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - UNSCOPED_INFO("3 glyphs for 'abc'"); REQUIRE(out.size() == 3u); for (const auto& g : out) { - UNSCOPED_INFO("each glyph has a face"); REQUIRE(g.face != nullptr); - UNSCOPED_INFO("each glyph has a glyph_id"); REQUIRE(g.glyph_id != 0u); - UNSCOPED_INFO("each glyph has a positive x_advance"); REQUIRE(g.x_advance > 0.f); } // Identity check: the shape's glyph_id for each ASCII char // must match the face's cmap-direct lookup. A regression that // bound HB to the wrong face would produce ids that disagree. - UNSCOPED_INFO("glyph[0] id matches cmap('a')"); CHECK(out[0].glyph_id == ft->getCharGlyphIndex(L'a')); - UNSCOPED_INFO("glyph[1] id matches cmap('b')"); CHECK(out[1].glyph_id == ft->getCharGlyphIndex(L'b')); - UNSCOPED_INFO("glyph[2] id matches cmap('c')"); CHECK(out[2].glyph_id == ft->getCharGlyphIndex(L'c')); } @@ -173,7 +164,6 @@ namespace if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("DejaVuSans loaded"); REQUIRE(ft.notNull()); const Text s = text('X','Y','a','b','c'); @@ -181,17 +171,13 @@ namespace const size_t end = s.at(5); std::vector shape_run_out; ALFontShaping::shapeRun(ft, s, begin, end, shape_run_out); - UNSCOPED_INFO("shapeRun produced 3 glyphs"); REQUIRE(shape_run_out.size() == 3u); // 'a' starts where the slice does. - UNSCOPED_INFO("shapeRun cluster[0] is an original-string offset"); CHECK(shape_run_out[0].cluster == (S32)begin); const auto& shape_line_out = ALFontShaping::shapeLine(ft, s, begin, end); - UNSCOPED_INFO("shapeLine produced 3 glyphs"); REQUIRE(shape_line_out.size() == 3u); // shapeLine clusters are slice-local: 'a' is at slice offset 0. - UNSCOPED_INFO("shapeLine cluster[0] is slice-local offset 0"); CHECK(shape_line_out[0].cluster == 0); } @@ -206,7 +192,6 @@ namespace if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("DejaVuSans loaded"); REQUIRE(ft.notNull()); const Text s = text('h','e','l','l','o'); @@ -215,15 +200,12 @@ namespace std::vector second; ALFontShaping::shapeRun(ft, s, 0, s.size(), second); - UNSCOPED_INFO("cache hit produces same glyph count"); REQUIRE(second.size() == first.size()); for (size_t i = 0; i < first.size(); ++i) { - UNSCOPED_INFO("cache hit: same glyph_id"); + CAPTURE(i); REQUIRE(second[i].glyph_id == first[i].glyph_id); - UNSCOPED_INFO("cache hit: same cluster"); REQUIRE(second[i].cluster == first[i].cluster); - UNSCOPED_INFO("cache hit: same x_advance"); REQUIRE(second[i].x_advance == first[i].x_advance); } } @@ -246,18 +228,13 @@ namespace LLPointer a = loadFt(a_path); LLPointer b = loadFt(b_path); - { - INFO("both faces loaded"); - REQUIRE(a.notNull()); - REQUIRE(b.notNull()); - } + REQUIRE(a.notNull()); + REQUIRE(b.notNull()); const Text s = text('a','b'); const auto& a_first = ALFontShaping::shapeLine(a, s, 0, s.size()); const auto& b_first = ALFontShaping::shapeLine(b, s, 0, s.size()); - UNSCOPED_INFO("a shape non-empty"); REQUIRE_FALSE(a_first.empty()); - UNSCOPED_INFO("b shape non-empty"); REQUIRE_FALSE(b_first.empty()); const void* a_addr0 = a_first.data(); const void* b_addr0 = b_first.data(); @@ -265,7 +242,7 @@ namespace // Hit A again: cache hit returns same storage address. const auto& a_second = ALFontShaping::shapeLine(a, s, 0, s.size()); UNSCOPED_INFO("A cache hit: same backing storage"); - REQUIRE(a_second.data() == a_addr0); + CHECK(a_second.data() == a_addr0); ALFontShaping::clearCacheForFace(a); @@ -274,12 +251,11 @@ namespace // address by the allocator after clear is implementation- // defined and not part of the contract.) const auto& a_after = ALFontShaping::shapeLine(a, s, 0, s.size()); - UNSCOPED_INFO("A re-shape produces output"); - REQUIRE_FALSE(a_after.empty()); + CHECK_FALSE(a_after.empty()); const auto& b_after = ALFontShaping::shapeLine(b, s, 0, s.size()); UNSCOPED_INFO("B cache survived clearCacheForFace(A)"); - REQUIRE(b_after.data() == b_addr0); + CHECK(b_after.data() == b_addr0); } // ZWJ heart-on-fire: U+2764 U+200D U+1F525 must collapse to a @@ -292,17 +268,14 @@ namespace if (!fileExists(path)) SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("Noto-COLRv1 loaded"); REQUIRE(ft.notNull()); // U+2764 (heart) U+200D (ZWJ) U+1F525 (fire) const Text s = text(0x2764, 0x200D, 0x1F525); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - UNSCOPED_INFO("heart-on-fire shaped to non-empty output"); REQUIRE_FALSE(out.empty()); - UNSCOPED_INFO("heart-on-fire ZWJ ligated to a single glyph"); - REQUIRE(out.size() == 1u); + CHECK(out.size() == 1u); } // VS-16 strip path: shaping U+2764 U+FE0F through Noto-COLRv1 @@ -315,13 +288,11 @@ namespace if (!fileExists(path)) SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("Noto-COLRv1 loaded"); REQUIRE(ft.notNull()); // Sanity: confirm VS-16 is genuinely absent from this font's // cmap (otherwise the strip path doesn't fire and the test // verifies nothing useful). - UNSCOPED_INFO("Noto-COLRv1 cmap lacks U+FE0F"); CHECK(ft->getCharGlyphIndex(0xFE0F) == 0u); const Text with_vs = text(0x2764, 0xFE0F); @@ -332,13 +303,11 @@ namespace ALFontShaping::shapeRun(ft, with_vs, 0, with_vs.size(), with_out); ALFontShaping::shapeRun(ft, without_vs, 0, without_vs.size(), without_out); - UNSCOPED_INFO("with-VS and without-VS produce same glyph count"); REQUIRE(with_out.size() == without_out.size()); - UNSCOPED_INFO("with-VS produced at least one glyph"); REQUIRE_FALSE(with_out.empty()); for (size_t i = 0; i < with_out.size(); ++i) { - UNSCOPED_INFO("with-VS glyph_id matches without-VS"); + CAPTURE(i); REQUIRE(with_out[i].glyph_id == without_out[i].glyph_id); } } @@ -359,12 +328,10 @@ namespace if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("DejaVuSans loaded"); REQUIRE(ft.notNull()); const Text s = text('h','i'); const auto& first = ALFontShaping::shapeLine(ft, s, 0, s.size()); - UNSCOPED_INFO("shape produced output"); REQUIRE_FALSE(first.empty()); UNSCOPED_INFO("first shape leaves one entry"); CHECK(ALFontShaping::cacheSize() == 1u); @@ -378,24 +345,22 @@ namespace // Cache hit: same storage AND the entry count stays at one. const auto& second = ALFontShaping::shapeLine(ft, s, 0, s.size()); UNSCOPED_INFO("cache hit storage is stable"); - REQUIRE(second.data() == addr0); + CHECK(second.data() == addr0); UNSCOPED_INFO("cache hit doesn't add an entry"); CHECK(ALFontShaping::cacheSize() == 1u); ALFontShaping::clearCache(); - UNSCOPED_INFO("clearCache empties the LRU"); CHECK(ALFontShaping::cacheSize() == 0u); // Miss: re-shape repopulates the cache. Output content is // identical (shaping is deterministic given the same face + // codepoints); the cache state is what changed. const auto& after = ALFontShaping::shapeLine(ft, s, 0, s.size()); - UNSCOPED_INFO("post-clear rebuild produced output"); REQUIRE_FALSE(after.empty()); UNSCOPED_INFO("post-clear miss repopulated to one entry"); CHECK(ALFontShaping::cacheSize() == 1u); - UNSCOPED_INFO("post-clear shape is content-identical"); - REQUIRE(after.size() == first_size); + UNSCOPED_INFO("post-clear shape has as many glyphs as the first"); + CHECK(after.size() == first_size); } // Multi-hop shape run: ASCII + CJK + Emoji through a head (DejaVu) @@ -414,12 +379,9 @@ namespace LLPointer head = loadFt(dejavu); LLPointer cjk = loadFt(han); LLPointer emo = loadFt(emoji); - { - INFO("all loaded"); - REQUIRE(head.notNull()); - REQUIRE(cjk.notNull()); - REQUIRE(emo.notNull()); - } + REQUIRE(head.notNull()); + REQUIRE(cjk.notNull()); + REQUIRE(emo.notNull()); head->addFallbackFont(cjk); head->addFallbackFont(emo); @@ -427,16 +389,12 @@ namespace const Text s = text('a', 0x4F60, 0x1F525); std::vector out; ALFontShaping::shapeRun(head, s, 0, s.size(), out); - UNSCOPED_INFO("3 glyphs (one per codepoint)"); REQUIRE(out.size() == 3u); // Each glyph carries the face it came from — verify the routing. - UNSCOPED_INFO("'a' routed to head (DejaVu)"); - REQUIRE(out[0].face == head.get()); - UNSCOPED_INFO("CJK routed to SourceHanSans"); - REQUIRE(out[1].face == cjk.get()); - UNSCOPED_INFO("emoji routed to Noto-COLRv1"); - REQUIRE(out[2].face == emo.get()); + CHECK(out[0].face == head.get()); + CHECK(out[1].face == cjk.get()); + CHECK(out[2].face == emo.get()); // All three glyphs should have non-zero ids (no notdef anywhere). UNSCOPED_INFO("'a' resolved to non-zero glyph"); CHECK(out[0].glyph_id != 0u); @@ -458,7 +416,6 @@ namespace if (!fileExists(path)) SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("Noto-COLRv1 loaded"); REQUIRE(ft.notNull()); // Heart + VS-16 + ZWJ + fire. The second codepoint (FE0F) gets @@ -468,7 +425,6 @@ namespace const Text s = text(0x2764, 0xFE0F, 0x200D, 0x1F525); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - UNSCOPED_INFO("output non-empty"); REQUIRE_FALSE(out.empty()); // Whatever the final shape (ZWJ ligature collapses to 1 glyph, // or 2 with VS-16 stripped), every cluster must point into the @@ -478,11 +434,8 @@ namespace // the strip MUST rebase past it. for (const auto& g : out) { - { - INFO("cluster points into original string range"); - REQUIRE(g.cluster >= 0); - REQUIRE(g.cluster < (S32)s.size()); - } + REQUIRE(g.cluster >= 0); + REQUIRE(g.cluster < (S32)s.size()); UNSCOPED_INFO("cluster does NOT point at stripped VS-16 position"); REQUIRE(g.cluster != (S32)s.at(1)); } @@ -502,11 +455,8 @@ namespace LLPointer a = loadFt(a_path); LLPointer b = loadFt(b_path); - { - INFO("both loaded"); - REQUIRE(a.notNull()); - REQUIRE(b.notNull()); - } + REQUIRE(a.notNull()); + REQUIRE(b.notNull()); a->addFallbackFont(b); // Shape via A — its entry's glyph stream contains a B glyph @@ -514,9 +464,7 @@ namespace const Text s = text('a', 0x4F60); const auto& a_first = ALFontShaping::shapeLine(a, s, 0, s.size()); const auto& b_first = ALFontShaping::shapeLine(b, s, 0, s.size()); - UNSCOPED_INFO("a non-empty"); REQUIRE_FALSE(a_first.empty()); - UNSCOPED_INFO("b non-empty"); REQUIRE_FALSE(b_first.empty()); const void* a_addr = a_first.data(); const void* b_addr = b_first.data(); @@ -529,15 +477,16 @@ namespace // not load-bearing for the contract; B's survival is.) const auto& a_after = ALFontShaping::shapeLine(a, s, 0, s.size()); const auto& b_after = ALFontShaping::shapeLine(b, s, 0, s.size()); - UNSCOPED_INFO("A re-shape produces output"); - REQUIRE_FALSE(a_after.empty()); + CHECK_FALSE(a_after.empty()); + // b_first is a reference into the cache: it dangles unless B's + // entry survived, so this REQUIRE guards the read below. UNSCOPED_INFO("B's entry survived clearCacheForFace(A)"); REQUIRE(b_after.data() == b_addr); // Sanity: the data we read out of the survived B entry is the // same B stream we observed before — a hidden cross-purge would // have reset it. UNSCOPED_INFO("B output unchanged across A's cache flush"); - REQUIRE(b_after.size() == b_first.size()); + CHECK(b_after.size() == b_first.size()); } // notdef contract: shaping a codepoint through a head with NO @@ -551,7 +500,6 @@ namespace if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("DejaVuSans loaded"); REQUIRE(ft.notNull()); // U+1F525 (fire emoji) is not in DejaVuSans's cmap, and we @@ -559,9 +507,7 @@ namespace const Text s = text(0x1F525); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - UNSCOPED_INFO("uncovered codepoint produces 1 glyph"); REQUIRE(out.size() == 1u); - UNSCOPED_INFO("uncovered codepoint glyph_id == 0 (notdef)"); CHECK(out[0].glyph_id == 0u); } @@ -581,17 +527,13 @@ namespace LLPointer head = loadFt(head_path); LLPointer emoji = loadFt(emoji_path); - { - INFO("both loaded"); - REQUIRE(head.notNull()); - REQUIRE(emoji.notNull()); - } + REQUIRE(head.notNull()); + REQUIRE(emoji.notNull()); head->addFallbackFont(emoji); const Text s = text('9', 0xFE0F, 0x20E3); std::vector out; ALFontShaping::shapeRun(head, s, 0, s.size(), out); - UNSCOPED_INFO("keycap shaped to non-empty output"); REQUIRE_FALSE(out.empty()); // The emoji face has the GSUB rule digit + U+20E3 -> keycap glyph // (VS-16 stripped pre-shape). All output glyphs must come from @@ -600,7 +542,6 @@ namespace // '9' glyph on `head` and only the keycap mark on `emoji`. for (const auto& g : out) { - UNSCOPED_INFO("every keycap-cluster glyph routes to emoji face"); REQUIRE(g.face == emoji.get()); } // Strong pin: the GSUB rule actually fires across the face @@ -608,8 +549,7 @@ namespace // but GSUB doesn't collapse — would leave 2+ glyphs on the // emoji face (the '9' outline plus an unbound keycap mark), // matching exactly what the user sees in the buggy report. - UNSCOPED_INFO("keycap '9️⃣' GSUB-collapsed to a single glyph"); - REQUIRE(out.size() == 1u); + CHECK(out.size() == 1u); } // ZWJ heart-on-fire across face boundary: U+2764 (BMP heart, present @@ -626,11 +566,8 @@ namespace LLPointer head = loadFt(head_path); LLPointer emoji = loadFt(emoji_path); - { - INFO("both loaded"); - REQUIRE(head.notNull()); - REQUIRE(emoji.notNull()); - } + REQUIRE(head.notNull()); + REQUIRE(emoji.notNull()); head->addFallbackFont(emoji); // Sanity: head DOES carry U+2764 — selectShapingFace would @@ -642,21 +579,17 @@ namespace const Text s = text(0x2764, 0xFE0F, 0x200D, 0x1F525); std::vector out; ALFontShaping::shapeRun(head, s, 0, s.size(), out); - UNSCOPED_INFO("heart-on-fire shaped to non-empty output"); REQUIRE_FALSE(out.empty()); for (const auto& g : out) { - UNSCOPED_INFO("every heart-on-fire glyph routes to emoji face"); REQUIRE(g.face == emoji.get()); } // Pin the ligature too — Noto-COLRv1's GSUB collapses the - // sequence to a single glyph. Test 6 covers the same rule when - // shaping directly through Noto; this asserts it still fires when - // the head is a non-emoji font and the cluster is routed across - // the fallback boundary. - UNSCOPED_INFO("heart-on-fire ZWJ ligated to a single glyph " - "even across the fallback boundary"); - REQUIRE(out.size() == 1u); + // sequence to a single glyph. The first heart-on-fire case covers + // the same rule when shaping directly through Noto; this asserts + // it still fires when the head is a non-emoji font and the + // cluster is routed across the fallback boundary. + CHECK(out.size() == 1u); } // Bare digit control: '9' alone (no FE0F + 20E3 follower) is NOT a @@ -671,28 +604,22 @@ namespace LLPointer head = loadFt(head_path); LLPointer emoji = loadFt(emoji_path); - { - INFO("both loaded"); - REQUIRE(head.notNull()); - REQUIRE(emoji.notNull()); - } + REQUIRE(head.notNull()); + REQUIRE(emoji.notNull()); head->addFallbackFont(emoji); const Text s = text('9'); std::vector out; ALFontShaping::shapeRun(head, s, 0, s.size(), out); - UNSCOPED_INFO("bare '9' produces one glyph"); REQUIRE(out.size() == 1u); - UNSCOPED_INFO("bare '9' routes to head (not emoji fallback)"); - REQUIRE(out[0].face == head.get()); - UNSCOPED_INFO("bare '9' glyph_id matches head's cmap"); + CHECK(out[0].face == head.get()); CHECK(out[0].glyph_id == head->getCharGlyphIndex(L'9')); } // Hash-keycap directly through the emoji face: pin that Noto-COLRv1 // actually has the GSUB rule for '#' + VS-16 + U+20E3 -> composed - // keycap (one glyph). The '9' rule is exercised by test 13 across - // a face boundary; this version uses Noto-COLRv1 directly so a font- + // keycap (one glyph). The '9' rule is exercised by the keycap 9 case + // across a face boundary; this version uses Noto-COLRv1 directly so a font- // level missing rule is observable as glyph_count > 1 (not a routing // bug). User report: '#️⃣' renders as '#' + a standalone keycap mark // — diagnosed by this test as either a font-rule miss or a routing @@ -703,27 +630,22 @@ namespace if (!fileExists(path)) SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("Noto-COLRv1 loaded"); REQUIRE(ft.notNull()); // Sanity: the font carries '#' and U+20E3 in cmap. If either // were missing, hb_shape would emit notdef before any GSUB // could match. - UNSCOPED_INFO("Noto-COLRv1 has '#' in cmap"); CHECK(ft->getCharGlyphIndex(L'#') != 0u); - UNSCOPED_INFO("Noto-COLRv1 has U+20E3 in cmap"); CHECK(ft->getCharGlyphIndex(0x20E3) != 0u); const Text s = text('#', 0xFE0F, 0x20E3); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - UNSCOPED_INFO("hash-keycap shaped to non-empty output"); REQUIRE_FALSE(out.empty()); // If the GSUB keycap rule fires, the run collapses to a single - // composed glyph (the same shape test 6 / test 13 expect for - // their respective ligatures). - UNSCOPED_INFO("hash-keycap '#️⃣' GSUB-collapsed to a single glyph"); - REQUIRE(out.size() == 1u); + // composed glyph (the same shape the heart-on-fire and keycap 9 + // cases expect for their respective ligatures). + CHECK(out.size() == 1u); } // Adjacent clusters: '3️⃣' (keycap) + '🏳️‍🌈' (pride flag, ZWJ @@ -743,11 +665,8 @@ namespace LLPointer head = loadFt(head_path); LLPointer emoji = loadFt(emoji_path); - { - INFO("both loaded"); - REQUIRE(head.notNull()); - REQUIRE(emoji.notNull()); - } + REQUIRE(head.notNull()); + REQUIRE(emoji.notNull()); head->addFallbackFont(emoji); // '3' VS-16 U+20E3 (keycap), then U+1F3F3 (waving white flag) @@ -756,11 +675,9 @@ namespace 0x1F3F3, 0xFE0F, 0x200D, 0x1F308); std::vector out; ALFontShaping::shapeRun(head, s, 0, s.size(), out); - UNSCOPED_INFO("adjacent clusters shaped to non-empty output"); REQUIRE_FALSE(out.empty()); for (const auto& g : out) { - UNSCOPED_INFO("every glyph routes to emoji face"); REQUIRE(g.face == emoji.get()); } // Both clusters compose: keycap '3️⃣' -> 1 glyph, pride flag @@ -769,7 +686,7 @@ namespace // (e.g. the pride flag's GSUB matching against the keycap mark // and falling back to bare white-flag + standalone rainbow). UNSCOPED_INFO("each adjacent cluster collapses independently"); - REQUIRE(out.size() == 2u); + CHECK(out.size() == 2u); } // Cluster with no emoji-face coverage: route to root rather than to @@ -789,27 +706,20 @@ namespace LLPointer head = loadFt(head_path); LLPointer fallback = loadFt(fallback_path); - { - INFO("both loaded"); - REQUIRE(head.notNull()); - REQUIRE(fallback.notNull()); - } + REQUIRE(head.notNull()); + REQUIRE(fallback.notNull()); head->addFallbackFont(fallback); // Sanity: neither face has U+20E3 (keycap mark) — that's the // scenario this test exercises. Neither has the keycap GSUB // rule. Best-case outcome: the base '#' renders through root. - UNSCOPED_INFO("Inter lacks U+20E3"); CHECK(head->getCharGlyphIndex(0x20E3) == 0u); - UNSCOPED_INFO("DejaVu lacks U+20E3"); CHECK(fallback->getCharGlyphIndex(0x20E3) == 0u); - UNSCOPED_INFO("Inter has '#' in cmap"); CHECK(head->getCharGlyphIndex(L'#') != 0u); const Text s = text('#', 0xFE0F, 0x20E3); std::vector out; ALFontShaping::shapeRun(head, s, 0, s.size(), out); - UNSCOPED_INFO("shape produced output"); REQUIRE_FALSE(out.empty()); // The base '#' must end up on a face that has it (root). A // regression that routed to a non-covering face would tofu @@ -840,11 +750,8 @@ namespace LLPointer head = loadFt(head_path); LLPointer emoji = loadFt(emoji_path); - { - INFO("both loaded"); - REQUIRE(head.notNull()); - REQUIRE(emoji.notNull()); - } + REQUIRE(head.notNull()); + REQUIRE(emoji.notNull()); head->addFallbackFont(emoji); // 🏴 (U+1F3F4) + 'g' 'b' 'e' 'n' 'g' tag chars + U+E007F. @@ -853,7 +760,6 @@ namespace 0xE007F); std::vector out; ALFontShaping::shapeRun(head, s, 0, s.size(), out); - UNSCOPED_INFO("subdivision flag shaped to non-empty output"); REQUIRE_FALSE(out.empty()); // Routing assertion is the load-bearing one: every cluster // member must land on the emoji face. A predicate regression @@ -861,7 +767,6 @@ namespace // the tag bytes onto root. for (const auto& g : out) { - UNSCOPED_INFO("every tag-flag glyph routes to emoji face"); REQUIRE(g.face == emoji.get()); } // Stretch goal: if Noto-COLRv1 carries the GB-ENG subdivision @@ -870,7 +775,7 @@ namespace // up to N glyphs, but never MORE than the input length (which // would mean glyph duplication). UNSCOPED_INFO("output glyph count never exceeds input codepoint count"); - REQUIRE(out.size() <= s.size()); + CHECK(out.size() <= s.size()); } // Cell-alignment probe. For each monospace face shipped in @@ -890,8 +795,8 @@ namespace "SourceCodeVF-Upright.woff2", }; - // Mixed ASCII pool to exercise more lookups than test 2's - // simple AV pair: alphanumerics, punctuation, brackets, + // Mixed ASCII pool to exercise more lookups than the simple AV + // pair in alfontshaping_gl_test.cpp: alphanumerics, punctuation, brackets, // symbols. Repeated to 200 chars so per-glyph drift // accumulates measurably. const std::string ascii_pool = @@ -912,16 +817,14 @@ namespace const std::string path = std::string(kFontDir) + fname; if (!fileExists(path)) continue; + CAPTURE(fname); LLPointer ft = loadFt(path); - UNSCOPED_INFO("monospace font loaded"); REQUIRE(ft.notNull()); - UNSCOPED_INFO("font is fixed-width"); REQUIRE(ft->isFixedWidth()); ft->setAllowMonospaceLigatures(true); // route through HB std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - UNSCOPED_INFO("N codepoints produce N glyphs through HB"); REQUIRE(out.size() == N); // Reference: first glyph's advance. In a fixed-width face @@ -929,10 +832,8 @@ namespace // sets the canonical cell. Deliberately don't use // ft->getXAdvance — that path asserts !mIsFallback and // the probe loads as fallback to skip GL pre-warm. - UNSCOPED_INFO("at least one glyph"); REQUIRE_FALSE(out.empty()); const F32 ref_advance = out[0].x_advance; - UNSCOPED_INFO("reference advance positive"); REQUIRE(ref_advance > 0.f); F32 max_dev = 0.f; @@ -967,16 +868,12 @@ namespace // Tolerance: 0.5px is generous — meaningful column drift // shows as ~1px by char ~50 in production reports. - UNSCOPED_INFO("per-glyph advance drift < 0.5px"); REQUIRE(max_dev < 0.5f); - UNSCOPED_INFO("cumulative advance drift < 0.5px"); REQUIRE(cumulative_dev < 0.5f); // Bypass writes 0 offsets; HB output should match for // ASCII (no GPOS positioning adjustments fire on monospace // ASCII with kern off). - UNSCOPED_INFO("HB ASCII x_offset is zero"); REQUIRE(max_xoff < 0.5f); - UNSCOPED_INFO("HB ASCII y_offset is zero"); REQUIRE(max_yoff < 0.5f); ++fonts_tested; } @@ -1019,13 +916,11 @@ namespace const std::string path = std::string(kFontDir) + fname; if (!fileExists(path)) continue; + CAPTURE(fname); LLPointer ft = loadFt(path); - UNSCOPED_INFO("monospace font loaded"); REQUIRE(ft.notNull()); - UNSCOPED_INFO("font is fixed-width"); REQUIRE(ft->isFixedWidth()); // Default: ligatures off. Routes through HB with kFixedWidthStrict. - UNSCOPED_INFO("ligatures default off"); REQUIRE_FALSE(ft->getAllowMonospaceLigatures()); // All ASCII, so a byte is a codepoint here too. @@ -1033,7 +928,6 @@ namespace std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - UNSCOPED_INFO("strict-mono glyph_count == codepoint_count"); REQUIRE(out.size() == s.size()); ++fonts_tested; } @@ -1058,9 +952,7 @@ namespace if (!fileExists(path)) SKIP("DejaVuSansMono.woff2 not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("DejaVuSansMono loaded"); REQUIRE(ft.notNull()); - UNSCOPED_INFO("DejaVuSansMono is fixed-width"); CHECK(ft->isFixedWidth()); const Text s = text(L'a', 0x0301); // a + COMBINING ACUTE ACCENT @@ -1075,7 +967,6 @@ namespace k, out[k].glyph_id, out[k].cluster, out[k].x_advance, out[k].x_offset, out[k].y_offset); } - UNSCOPED_INFO("combining cluster produces at least 1 glyph"); REQUIRE_FALSE(out.empty()); // Expected outcomes: // (a) ccmp composed to precomposed 'á' → 1 glyph, full cell advance. @@ -1085,7 +976,6 @@ namespace // Either is correct GPOS-driven behavior; the bypass produced // neither (it emitted 2 glyphs, both with full advance). const F32 ref_advance = out[0].x_advance; - UNSCOPED_INFO("base glyph has positive advance"); CHECK(ref_advance > 0.f); if (out.size() == 1) { @@ -1111,7 +1001,7 @@ namespace } // ----------------------------------------------------------------- - // FT/HB consistency block (tests 23-31). Locks the integration + // FT/HB consistency block (the next eight cases). Locks the integration // invariants between FreeType and HarfBuzz so a refactor that breaks // size, scale, glyph identity, hinting, variation axes, cmap // selection, or stem-darkening surfaces immediately. Every test @@ -1144,14 +1034,11 @@ namespace if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("DejaVuSans loaded"); REQUIRE(ft.notNull()); const ALFontFace* face = ft->getFontFace(); - UNSCOPED_INFO("face accessible"); REQUIRE(face != nullptr); hb_font_t* hbf = face->getHbFont(); - UNSCOPED_INFO("hb_font accessible"); REQUIRE(hbf != nullptr); int hb_x_scale = 0, hb_y_scale = 0; @@ -1163,22 +1050,17 @@ namespace // avoid 32-bit overflow at large UPEM × scale products. const int expected_x = (int)((((std::uint64_t)m.x_scale * (std::uint64_t)ftf->units_per_EM) + (1u << 15)) >> 16); const int expected_y = (int)((((std::uint64_t)m.y_scale * (std::uint64_t)ftf->units_per_EM) + (1u << 15)) >> 16); - UNSCOPED_INFO("HB x_scale matches hb-ft derivation from FT state"); CHECK(hb_x_scale == expected_x); - UNSCOPED_INFO("HB y_scale matches hb-ft derivation from FT state"); CHECK(hb_y_scale == expected_y); // Sanity: scale must be positive (zero would mean uninitialized). - UNSCOPED_INFO("HB x_scale is positive"); CHECK(hb_x_scale > 0); - UNSCOPED_INFO("HB y_scale is positive"); CHECK(hb_y_scale > 0); } - // (test<24> intentionally absent — formerly checked hb_font_get_ppem + // (Audit test 24 intentionally absent — it checked hb_font_get_ppem // against FT ppem, but hb_ft_font_create_referenced doesn't set - // ppem on the HB font; only scale carries the size invariant. - // Keeping the gap so the audit's test numbering (23 + 25..31) - // matches the plan file unchanged.) + // ppem on the HB font; only scale carries the size invariant. This + // block is the audit's tests 23 and 25..31, in that order.) // For an unligatured / unkerned ASCII run, HB's per-glyph // x_advance must equal FT's slot->advance.x for the same glyph @@ -1192,7 +1074,6 @@ namespace if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("DejaVuSans loaded"); REQUIRE(ft.notNull()); const ALFontFace* face = ft->getFontFace(); @@ -1202,19 +1083,17 @@ namespace const Text s = text('a','b','c','d','e','f'); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - UNSCOPED_INFO("6 glyphs for 'abcdef'"); REQUIRE(out.size() == 6u); for (size_t i = 0; i < out.size(); ++i) { - UNSCOPED_INFO("FT_Load_Glyph succeeded"); + CAPTURE(i); REQUIRE(FT_Load_Glyph(ftf, out[i].glyph_id, load_flags) == 0); const F32 ft_advance = ftf->glyph->advance.x * (1.f / 64.f); // 26.6 -> float; equality in float at this precision // would be exact when no GPOS adjustment fired. Tolerate // 1/64 px to cover HB's internal rounding paths. const F32 delta = std::fabs(out[i].x_advance - ft_advance); - UNSCOPED_INFO("HB x_advance matches FT advance for unkerned glyph"); REQUIRE(delta < (1.f / 64.f) + 1e-5f); } } @@ -1230,21 +1109,17 @@ namespace if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("DejaVuSans loaded"); REQUIRE(ft.notNull()); const ALFontFace* face = ft->getFontFace(); hb_font_t* hbf = face->getHbFont(); - UNSCOPED_INFO("hb_font accessible"); REQUIRE(hbf != nullptr); const int hb_flags = hb_ft_font_get_load_flags(hbf); const int expected = static_cast(face->hinting()); - UNSCOPED_INFO("HB load flags == (int)mHinting"); CHECK(hb_flags == expected); // And confirm the cast equivalence between the two surfaces // (HB takes int, FT takes FT_Int32) hasn't drifted. - UNSCOPED_INFO("(int)mHinting == (FT_Int32)mHinting"); CHECK(static_cast(face->hinting()) == (int)static_cast(face->hinting())); } @@ -1266,8 +1141,8 @@ namespace ALFontVarAxes va; va.wght = 600.f; va.wght_set = true; LLPointer ft = new LLFontFreetype; - UNSCOPED_INFO("InterVariable loaded at weight=600"); - CHECK(ft->loadFace(path, /*point_size=*/14.f, + // A failed load leaves no face for the reads below. + REQUIRE(ft->loadFace(path, /*point_size=*/14.f, /*vert_dpi=*/96.f, /*horz_dpi=*/96.f, /*is_fallback=*/true, /*face_n=*/0, EFontHinting::DEFAULT, @@ -1276,36 +1151,30 @@ namespace const ALFontFace* face = ft->getFontFace(); FT_Face ftf = face->face(); hb_font_t* hbf = face->getHbFont(); - UNSCOPED_INFO("hb_font accessible"); REQUIRE(hbf != nullptr); FT_MM_Var* mm = nullptr; - UNSCOPED_INFO("FT_Get_MM_Var on a variable font"); CHECK(FT_Get_MM_Var(ftf, &mm) == 0); - UNSCOPED_INFO("FT_MM_Var allocated"); REQUIRE(mm != nullptr); const FT_UInt num_axis = mm->num_axis; - UNSCOPED_INFO("variable font reports >= 1 axis"); CHECK(num_axis >= 1u); std::vector ft_coords(num_axis); - UNSCOPED_INFO("FT_Get_Var_Design_Coordinates"); CHECK(FT_Get_Var_Design_Coordinates(ftf, num_axis, ft_coords.data()) == 0); unsigned int hb_len = 0; const float* hb_coords = hb_font_get_var_coords_design(hbf, &hb_len); - UNSCOPED_INFO("HB has design coords set"); REQUIRE(hb_coords != nullptr); - UNSCOPED_INFO("HB axis count matches FT"); - CHECK(hb_len == (unsigned int)num_axis); + // The loop below reads hb_coords[i] for every FT axis. + REQUIRE(hb_len == (unsigned int)num_axis); for (FT_UInt i = 0; i < num_axis; ++i) { + CAPTURE(i); const float ft_design = ft_coords[i] / 65536.0f; // 16.16 -> design const float delta = std::fabs(hb_coords[i] - ft_design); // 16.16 round-trip: equality should be exact, but allow // 1/65536 to cover any internal float-conversion rounding. - UNSCOPED_INFO("HB design coord matches FT design coord"); REQUIRE(delta < (1.f / 65536.f) + 1e-6f); } @@ -1320,7 +1189,6 @@ namespace // stem darkening. TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: stem darkening is off on every FreeType module that has the property", "[ALFontShaping]") { - UNSCOPED_INFO("gFTLibrary initialized"); REQUIRE(gFTLibrary != nullptr); const char* modules[] = { "autofitter", "cff", "type1", "t1cid" }; @@ -1335,7 +1203,7 @@ namespace // a no-op. if (err != 0) continue; - UNSCOPED_INFO(std::string("no-stem-darkening enabled on ") + mod); + CAPTURE(mod); REQUIRE(no_darken != 0); } } @@ -1350,30 +1218,25 @@ namespace if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("DejaVuSans loaded"); REQUIRE(ft.notNull()); const ALFontFace* face = ft->getFontFace(); FT_Face ftf = face->face(); - UNSCOPED_INFO("active charmap is set"); REQUIRE(ftf->charmap != nullptr); - UNSCOPED_INFO("active charmap encoding is FT_ENCODING_UNICODE"); - CHECK((int)ftf->charmap->encoding == - (int)FT_ENCODING_UNICODE); + CHECK((int)ftf->charmap->encoding == (int)FT_ENCODING_UNICODE); } // VS-15 strip parity with VS-16: a face that lacks U+FE0E in cmap // must still produce identical shape output for `` // vs just `` — the do_shape strip pass should drop VS-15 // before HB so the notdef glyph for VS-15 doesn't sit between - // base and follow-up codepoints. Mirrors test<7>'s VS-16 proof. + // base and follow-up codepoints. Mirrors the VS-16 strip case's proof. TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a VS-15 the cmap lacks is stripped as VS-16 is", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("Noto-COLRv1 loaded"); REQUIRE(ft.notNull()); // Sanity: the test only verifies anything if Noto-COLRv1 also @@ -1389,20 +1252,18 @@ namespace ALFontShaping::shapeRun(ft, with_vs, 0, with_vs.size(), with_out); ALFontShaping::shapeRun(ft, without_vs, 0, without_vs.size(), without_out); - UNSCOPED_INFO("with-VS15 and without-VS15 produce same glyph count"); REQUIRE(with_out.size() == without_out.size()); - UNSCOPED_INFO("with-VS15 produced at least one glyph"); REQUIRE_FALSE(with_out.empty()); for (size_t i = 0; i < with_out.size(); ++i) { - UNSCOPED_INFO("with-VS15 glyph_id matches without-VS15"); + CAPTURE(i); REQUIRE(with_out[i].glyph_id == without_out[i].glyph_id); } } // Identity check between codepoint→glyph (cmap) and shape result - // for a non-Latin codepoint that DejaVuSans covers. test<2> covers - // ASCII; this extends the contract to the upper BMP so a regression + // for a non-Latin codepoint that DejaVuSans covers. The ASCII case + // covers ASCII; this extends the contract to the upper BMP so a regression // limited to Unicode-cmap binding (Defect B in the audit plan) is // caught even when ASCII still works. TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a non-ASCII codepoint shapes to the glyph the cmap gives it", "[ALFontShaping]") @@ -1411,7 +1272,6 @@ namespace if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("DejaVuSans loaded"); REQUIRE(ft.notNull()); // Pick the first non-ASCII codepoint the face has. DejaVuSans @@ -1432,11 +1292,8 @@ namespace const Text s = text(wch); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - UNSCOPED_INFO("1 glyph for non-ASCII codepoint"); REQUIRE(out.size() == 1u); - UNSCOPED_INFO("shape glyph_id matches cmap lookup"); CHECK(out[0].glyph_id == ft->getCharGlyphIndex(wch)); - UNSCOPED_INFO("non-ASCII glyph has positive advance"); CHECK(out[0].x_advance > 0.f); } @@ -1452,7 +1309,6 @@ namespace if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("DejaVuSans loaded"); REQUIRE(ft.notNull()); // begin/end are written as codepoint indices and converted per case. @@ -1469,6 +1325,7 @@ namespace for (size_t ci = 0; ci < cases.size(); ++ci) { + CAPTURE(ci); const auto& c = cases[ci]; const size_t begin_bytes = c.s.at(c.begin); const size_t end_bytes = c.s.at(c.end); @@ -1476,9 +1333,7 @@ namespace ALFontShaping::shapeRun(ft, c.s, begin_bytes, end_bytes, out); for (const auto& g : out) { - UNSCOPED_INFO(("cluster >= begin (case " + std::to_string(ci) + ")").c_str()); REQUIRE(g.cluster >= (S32)begin_bytes); - UNSCOPED_INFO(("cluster < end (case " + std::to_string(ci) + ")").c_str()); REQUIRE(g.cluster < (S32)end_bytes); } } @@ -1502,7 +1357,6 @@ namespace if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("DejaVuSans loaded"); REQUIRE(ft.notNull()); // "X" + trans flag (5 cps, [1, 6)) + "Y". The cluster walker @@ -1514,7 +1368,6 @@ namespace 0xFE0F, L'Y'); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - UNSCOPED_INFO("shape produced output"); REQUIRE_FALSE(out.empty()); // Every glyph whose cluster falls inside the flag must report the @@ -1526,7 +1379,7 @@ namespace { if (g.cluster >= flag_begin && g.cluster < flag_end) saw_cluster_glyph = true; - UNSCOPED_INFO("trans-flag cluster glyphs collapse to cluster start"); + CAPTURE(g.cluster, flag_begin, flag_end); REQUIRE_FALSE((g.cluster > flag_begin && g.cluster < flag_end)); } UNSCOPED_INFO("at least one glyph routed through the cluster sub-run " @@ -1553,22 +1406,16 @@ namespace LLPointer a = loadFt(a_path); LLPointer b = loadFt(b_path); - { - INFO("both loaded"); - REQUIRE(a.notNull()); - REQUIRE(b.notNull()); - } + REQUIRE(a.notNull()); + REQUIRE(b.notNull()); // Pre-fallback shape: U+4F60 (你) is not in DejaVuSans's cmap, so // HB returns one notdef glyph (glyph_id=0) on face A. const Text s = text(0x4F60); std::vector pre; ALFontShaping::shapeRun(a, s, 0, s.size(), pre); - UNSCOPED_INFO("pre-fallback shape produces 1 glyph"); REQUIRE(pre.size() == 1u); - UNSCOPED_INFO("pre-fallback glyph routes to head A"); - REQUIRE(pre[0].face == a.get()); - UNSCOPED_INFO("pre-fallback glyph is notdef (glyph_id=0)"); + CHECK(pre[0].face == a.get()); CHECK(pre[0].glyph_id == 0u); UNSCOPED_INFO("pre-fallback shape leaves one cache entry"); CHECK(ALFontShaping::cacheSize() == 1u); @@ -1584,11 +1431,8 @@ namespace // non-zero glyph_id (B has the CJK glyph in its cmap). std::vector post; ALFontShaping::shapeRun(a, s, 0, s.size(), post); - UNSCOPED_INFO("post-fallback shape produces 1 glyph"); REQUIRE(post.size() == 1u); - UNSCOPED_INFO("post-fallback glyph routes to fallback B"); - REQUIRE(post[0].face == b.get()); - UNSCOPED_INFO("post-fallback glyph is real (not notdef)"); + CHECK(post[0].face == b.get()); CHECK(post[0].glyph_id != 0u); } diff --git a/indra/llrender/tests/algpubuffer_gl_test.cpp b/indra/llrender/tests/algpubuffer_gl_test.cpp index f4f8fa9819..647cfda0c5 100644 --- a/indra/llrender/tests/algpubuffer_gl_test.cpp +++ b/indra/llrender/tests/algpubuffer_gl_test.cpp @@ -102,7 +102,7 @@ namespace } // A program that draws a point and declares no inputs, bound, for the indirect draws. - void setUpProgram() + void require_program() { const GLuint vs = ll_test::compileTestShader(GL_VERTEX_SHADER, kVertex); const GLuint fs = ll_test::compileTestShader(GL_FRAGMENT_SHADER, kFragment); @@ -118,9 +118,7 @@ namespace const bool linked = mProgram.mapAttributes(); glDeleteShader(vs); glDeleteShader(fs); - UNSCOPED_INFO("program links"); REQUIRE(linked); - UNSCOPED_INFO("program maps"); REQUIRE(mProgram.mapUniforms()); mProgram.bind(); } @@ -139,15 +137,10 @@ namespace std::vector expected = pattern(WORDS, 1000u); { ALGPUBuffer buffer; - UNSCOPED_INFO("allocates"); REQUIRE(buffer.allocate(WORDS * sizeof(U32), ALGPUBuffer::DYNAMIC, expected.data(), "words (test)")); - UNSCOPED_INFO("allocated"); CHECK(buffer.isAllocated()); - UNSCOPED_INFO("its size"); REQUIRE(buffer.size() == size_t(WORDS * sizeof(U32))); - UNSCOPED_INFO("its flags"); CHECK(buffer.getFlags() == U32(ALGPUBuffer::DYNAMIC)); - UNSCOPED_INFO("accounted"); CHECK(ALGPUBuffer::sBytesAllocated == before + WORDS * sizeof(U32)); UNSCOPED_INFO("holds its data"); CHECK(readAll(buffer) == expected); @@ -164,18 +157,14 @@ namespace CHECK(readAll(buffer) == expected); std::vector part(4, 0u); - UNSCOPED_INFO("reads at an offset"); REQUIRE(buffer.read(42 * sizeof(U32), part.data(), part.size() * sizeof(U32))); - UNSCOPED_INFO("the words there"); CHECK(part == std::vector(expected.begin() + 42, expected.begin() + 46)); buffer.clear(7u); - UNSCOPED_INFO("whole clear"); CHECK(readAll(buffer) == std::vector(WORDS, 7u)); } UNSCOPED_INFO("destruction returns what it accounted"); CHECK(ALGPUBuffer::sBytesAllocated == before); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -188,9 +177,7 @@ namespace std::vector expected(WORDS, 0u); ALGPUBuffer source; ALGPUBuffer target; - UNSCOPED_INFO("source allocates"); REQUIRE(source.allocate(WORDS * sizeof(U32), 0, source_words.data(), "source (test)")); - UNSCOPED_INFO("target allocates"); REQUIRE(target.allocate(WORDS * sizeof(U32), 0, expected.data(), "target (test)")); target.copyFrom(source, 4 * sizeof(U32), 32 * sizeof(U32), 10 * sizeof(U32)); @@ -202,16 +189,13 @@ namespace std::copy(expected.begin() + 32, expected.begin() + 42, expected.begin()); UNSCOPED_INFO("copied within one"); CHECK(readAll(target) == expected); - UNSCOPED_INFO("the source untouched"); CHECK(readAll(source) == source_words); if (clearsOnGPU()) { target.clear(0xA5A5A5A5u); - UNSCOPED_INFO("a static buffer clears"); CHECK(readAll(target) == std::vector(WORDS, 0xA5A5A5A5u)); } - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -223,37 +207,29 @@ namespace const std::vector words = pattern(WORDS, 9u); ALGPUBuffer first; - UNSCOPED_INFO("allocates"); REQUIRE(first.allocate(WORDS * sizeof(U32), 0, words.data(), "first (test)")); first.release(); - UNSCOPED_INFO("released"); CHECK_FALSE(first.isAllocated()); - UNSCOPED_INFO("no size"); - REQUIRE(first.size() == size_t(0)); - UNSCOPED_INFO("no name"); + CHECK(first.size() == size_t(0)); CHECK(first.getGLName() == 0u); UNSCOPED_INFO("accounting returned"); CHECK(ALGPUBuffer::sBytesAllocated == before); - UNSCOPED_INFO("allocates again"); - CHECK(first.allocate(WORDS * sizeof(U32), 0, words.data(), "first (test)")); + REQUIRE(first.allocate(WORDS * sizeof(U32), 0, words.data(), "first (test)")); const U32 name = first.getGLName(); ALGPUBuffer moved(std::move(first)); { INFO("the source is empty"); CHECK_FALSE(first.isAllocated()); - REQUIRE(first.size() == 0); + CHECK(first.size() == 0); CHECK(first.getGLName() == 0); } - UNSCOPED_INFO("the target took the name"); CHECK(moved.getGLName() == name); - UNSCOPED_INFO("and the store"); CHECK(readAll(moved) == words); UNSCOPED_INFO("accounted once"); CHECK(ALGPUBuffer::sBytesAllocated == before + WORDS * sizeof(U32)); ALGPUBuffer other; - UNSCOPED_INFO("other allocates"); REQUIRE(other.allocate(2 * WORDS * sizeof(U32), 0, nullptr, "other (test)")); other = std::move(moved); { @@ -261,9 +237,7 @@ namespace CHECK_FALSE(moved.isAllocated()); CHECK(moved.getGLName() == 0); } - UNSCOPED_INFO("the assigned took the name"); CHECK(other.getGLName() == name); - UNSCOPED_INFO("and the store"); CHECK(readAll(other) == words); UNSCOPED_INFO("its own store released"); CHECK(ALGPUBuffer::sBytesAllocated == before + WORDS * sizeof(U32)); @@ -271,7 +245,6 @@ namespace other.release(); UNSCOPED_INFO("all returned"); CHECK(ALGPUBuffer::sBytesAllocated == before); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -280,13 +253,12 @@ namespace // same name is bound rather than taken for bound already. TEST_CASE_METHOD(algpubuffer_data, "algpubuffer_gl: LLRender keeps an indirect draw's buffer bound and rebinds when it must", "[algpubuffer_gl]") { - setUpProgram(); + require_program(); ALDrawArraysIndirectCommand command; command.mCount = 1; command.mInstanceCount = 1; ALGPUBuffer first; - UNSCOPED_INFO("allocates"); REQUIRE(first.allocate(sizeof(command), 0, &command, "draw (test)")); gGL.drawArraysIndirect(LLRender::POINTS, first, 0); UNSCOPED_INFO("bound for the draw"); @@ -301,7 +273,7 @@ namespace gGL.drawArraysIndirect(LLRender::POINTS, first, 0); UNSCOPED_INFO("bound again after refreshState"); CHECK(indirectBinding() == (GLint)first.getGLName()); - UNSCOPED_INFO("no GL error after refreshState"); + UNSCOPED_INFO("after refreshState"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); const U32 name = first.getGLName(); @@ -309,7 +281,6 @@ namespace UNSCOPED_INFO("deleting it unbound it"); CHECK(indirectBinding() == 0); ALGPUBuffer second; - UNSCOPED_INFO("another allocates"); REQUIRE(second.allocate(sizeof(command), 0, &command, "draw again (test)")); if (second.getGLName() != name) { @@ -318,7 +289,6 @@ namespace gGL.drawArraysIndirect(LLRender::POINTS, second, 0); UNSCOPED_INFO("the reused name is bound"); CHECK(indirectBinding() == (GLint)second.getGLName()); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -330,23 +300,19 @@ namespace ALGPUBuffer readback; ALGPUBuffer stream; ALGPUBuffer check; - UNSCOPED_INFO("readback allocates"); REQUIRE(readback.allocate(WORDS * sizeof(U32), ALGPUBuffer::READBACK, nullptr, "readback (test)")); - UNSCOPED_INFO("stream allocates"); REQUIRE(stream.allocate(WORDS * sizeof(U32), ALGPUBuffer::STREAM, nullptr, "stream (test)")); - UNSCOPED_INFO("check allocates"); REQUIRE(check.allocate(WORDS * sizeof(U32), 0, nullptr, "check (test)")); UNSCOPED_INFO("readback mapped where persistent maps resolve"); - REQUIRE((readback.getMapped() != nullptr) == persistent); - UNSCOPED_INFO("stream too"); - REQUIRE((stream.getMapped() != nullptr) == persistent); + CHECK((readback.getMapped() != nullptr) == persistent); + UNSCOPED_INFO("stream mapped where persistent maps resolve"); + CHECK((stream.getMapped() != nullptr) == persistent); const std::vector words = pattern(WORDS, 300u); stream.upload(0, words.data(), words.size() * sizeof(U32)); check.copyFrom(stream, 0, 0, WORDS * sizeof(U32)); UNSCOPED_INFO("the GPU sees the stream's upload"); CHECK(readAll(check) == words); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -357,12 +323,9 @@ namespace constexpr U32 SLOTS = 3; constexpr U32 COUNT = 16; ALGPUBuffer source; - UNSCOPED_INFO("source allocates"); REQUIRE(source.allocate(WORDS * sizeof(U32), ALGPUBuffer::DYNAMIC, nullptr, "source (test)")); ALGPUReadback readback; - UNSCOPED_INFO("readback allocates"); REQUIRE(readback.allocate(COUNT * sizeof(U32), SLOTS, "readback (test)")); - UNSCOPED_INFO("a slot's size"); CHECK(readback.getSlotSize() == size_t(COUNT * sizeof(U32))); std::vector result(COUNT, 0u); @@ -372,23 +335,21 @@ namespace { const std::vector words = pattern(WORDS, 1000u * (i + 1)); source.upload(0, words.data(), words.size() * sizeof(U32)); - UNSCOPED_INFO("request " + std::to_string(i)); + CAPTURE(i); REQUIRE(readback.request(source, 8 * sizeof(U32), COUNT * sizeof(U32))); } - UNSCOPED_INFO("every slot pending"); CHECK(readback.getPending() == SLOTS); UNSCOPED_INFO("a request past them refused"); CHECK_FALSE(readback.request(source, 0, COUNT * sizeof(U32))); for (U32 i = 0; i < SLOTS; ++i) { - UNSCOPED_INFO("collect " + std::to_string(i)); + CAPTURE(i); REQUIRE(readback.collect(result.data(), result.size() * sizeof(U32), true)); const std::vector words = pattern(WORDS, 1000u * (i + 1)); - UNSCOPED_INFO("result " + std::to_string(i) + " is what the source held at its request"); + UNSCOPED_INFO("what the source held at its request"); REQUIRE(result == std::vector(words.begin() + 8, words.begin() + 8 + COUNT)); } - UNSCOPED_INFO("none pending"); CHECK(readback.getPending() == 0u); UNSCOPED_INFO("nothing more to collect"); REQUIRE_FALSE(readback.collect(result.data(), result.size() * sizeof(U32), true)); @@ -396,14 +357,12 @@ namespace // A request made, its copy finished: collect gives it without waiting. const std::vector last = pattern(WORDS, 7u); source.upload(0, last.data(), last.size() * sizeof(U32)); - UNSCOPED_INFO("one more request"); CHECK(readback.request(source, 0, COUNT * sizeof(U32))); glFinish(); UNSCOPED_INFO("a finished copy is collected without waiting"); REQUIRE(readback.collect(result.data(), result.size() * sizeof(U32))); UNSCOPED_INFO("and holds the source"); CHECK(result == std::vector(last.begin(), last.begin() + COUNT)); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -414,12 +373,9 @@ namespace constexpr U32 REGIONS = 3; constexpr U32 COUNT = 10; ALGPUStream stream; - UNSCOPED_INFO("stream allocates"); REQUIRE(stream.allocate(COUNT * sizeof(U32), REGIONS, "stream (test)")); - UNSCOPED_INFO("a region's size"); CHECK(stream.getRegionSize() == size_t(COUNT * sizeof(U32))); ALGPUBuffer check; - UNSCOPED_INFO("check allocates"); REQUIRE(check.allocate(COUNT * sizeof(U32), 0, nullptr, "check (test)")); const size_t alignment = gGLManager.mShaderStorageBufferOffsetAlignment > 0 @@ -428,18 +384,16 @@ namespace std::vector offsets; for (U32 frame = 0; frame < 2 * REGIONS; ++frame) { + CAPTURE(frame); U8* region = stream.map(); - UNSCOPED_INFO("a region to write"); REQUIRE(region != nullptr); const std::vector words = pattern(COUNT, 100u * (frame + 1)); memcpy(region, words.data(), words.size() * sizeof(U32)); const size_t offset = stream.publish(words.size() * sizeof(U32)); - UNSCOPED_INFO("frame " + std::to_string(frame) + " at an aligned offset"); REQUIRE(offset % alignment == 0); offsets.push_back(offset); check.copyFrom(stream.getBuffer(), offset, 0, COUNT * sizeof(U32)); - UNSCOPED_INFO("frame " + std::to_string(frame) + " reaches the GPU"); REQUIRE(readAll(check) == words); } for (U32 frame = 0; frame < REGIONS; ++frame) @@ -452,7 +406,6 @@ namespace UNSCOPED_INFO("the ring wraps to the same region"); REQUIRE(offsets[frame + REGIONS] == offsets[frame]); } - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } } diff --git a/indra/llrender/tests/alimageslot_gl_test.cpp b/indra/llrender/tests/alimageslot_gl_test.cpp index 196d5aa962..3d5c1545eb 100644 --- a/indra/llrender/tests/alimageslot_gl_test.cpp +++ b/indra/llrender/tests/alimageslot_gl_test.cpp @@ -119,16 +119,14 @@ namespace // Compute in use and two R32F targets with a mip level beyond the first, or the case skipped where the // context has no images. - void setUp() + void require_targets() { if (!ll_test::contextHasCompute()) { SKIP("the context has no compute"); } LLRender::sCompute = true; - UNSCOPED_INFO("target allocates"); REQUIRE(mTarget.allocate(SIZE, SIZE, GL_R32F, false, false, ALTextureSlot::TT_TEXTURE, LLRenderTarget::MIPS_MANUAL)); - UNSCOPED_INFO("other target allocates"); REQUIRE(mOther.allocate(SIZE, SIZE, GL_RGBA16F)); } @@ -144,56 +142,42 @@ namespace // issues one call, and unbind returns the unit to texture 0 once. TEST_CASE_METHOD(alimageslot_data, "alimageslot_gl: a bind reaches GL as stated and only a change issues a call", "[alimageslot_gl]") { - setUp(); + require_targets(); ALImageSlot* slot = gGL.getImageSlot(2); - UNSCOPED_INFO("the slot is its unit"); CHECK(slot->getIndex() == 2); U32 binds = ALImageSlot::sImageBinds; - UNSCOPED_INFO("bound"); CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_WRITE, 1)); - UNSCOPED_INFO("one call"); + UNSCOPED_INFO("the bind issues one call"); CHECK(ALImageSlot::sImageBinds == binds + 1); UnitState state = unitState(2); - UNSCOPED_INFO("GL holds the texture"); CHECK(state.mTexture == (GLint)mTarget.getTexture()); - UNSCOPED_INFO("at its level"); CHECK(state.mLevel == 1); - UNSCOPED_INFO("not layered"); CHECK(state.mLayered == GL_FALSE); - UNSCOPED_INFO("read-write"); CHECK(state.mAccess == GL_READ_WRITE); UNSCOPED_INFO("at the target's own format"); CHECK(state.mFormat == GL_R32F); - UNSCOPED_INFO("the slot says so"); CHECK(slot->getCurrTexture() == mTarget.getTexture()); binds = ALImageSlot::sImageBinds; - UNSCOPED_INFO("bound again"); CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_WRITE, 1)); UNSCOPED_INFO("the same bind issues nothing"); CHECK(ALImageSlot::sImageBinds == binds); - UNSCOPED_INFO("another access"); CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY, 1)); UNSCOPED_INFO("a new access issues one call"); CHECK(ALImageSlot::sImageBinds == binds + 1); - UNSCOPED_INFO("GL holds the new access"); CHECK(unitState(2).mAccess == GL_READ_ONLY); - UNSCOPED_INFO("another level"); CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY, 0)); UNSCOPED_INFO("a new level issues one call"); CHECK(ALImageSlot::sImageBinds == binds + 2); - UNSCOPED_INFO("GL holds the new level"); CHECK(unitState(2).mLevel == 0); - UNSCOPED_INFO("another texture"); CHECK(slot->bind(&mOther, 0, ALImageSlot::READ_ONLY, 0)); UNSCOPED_INFO("a new texture issues one call"); CHECK(ALImageSlot::sImageBinds == binds + 3); state = unitState(2); - UNSCOPED_INFO("GL holds the other texture"); CHECK(state.mTexture == (GLint)mOther.getTexture()); UNSCOPED_INFO("at its own format"); CHECK(state.mFormat == GL_RGBA16F); @@ -201,25 +185,20 @@ namespace slot->unbind(); UNSCOPED_INFO("unbind issues one call"); CHECK(ALImageSlot::sImageBinds == binds + 4); - UNSCOPED_INFO("GL holds no texture"); CHECK(unitState(2).mTexture == 0); - UNSCOPED_INFO("nor does the slot"); CHECK(slot->getCurrTexture() == 0u); slot->unbind(); UNSCOPED_INFO("unbinding nothing issues nothing"); CHECK(ALImageSlot::sImageBinds == binds + 4); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A unit changed behind LLRender's back is put back by refreshState, every part of it. TEST_CASE_METHOD(alimageslot_data, "alimageslot_gl: refreshState puts back every part of a unit changed behind its back", "[alimageslot_gl]") { - setUp(); - UNSCOPED_INFO("bound"); + require_targets(); CHECK(gGL.getImageSlot(3)->bind(&mTarget, 0, ALImageSlot::WRITE_ONLY, 1)); - UNSCOPED_INFO("other bound"); CHECK(gGL.getImageSlot(4)->bind(&mOther, 0, ALImageSlot::READ_ONLY)); glBindImageTexture(3, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R8); @@ -230,23 +209,21 @@ namespace gGL.refreshState(); UnitState state = unitState(3); - UNSCOPED_INFO("the texture put back"); - CHECK(state.mTexture == (GLint)mTarget.getTexture()); - UNSCOPED_INFO("its level put back"); - CHECK(state.mLevel == 1); - UNSCOPED_INFO("its access put back"); - CHECK(state.mAccess == GL_WRITE_ONLY); - UNSCOPED_INFO("its format put back"); - CHECK(state.mFormat == GL_R32F); + { + INFO("unit 3 put back"); + CHECK(state.mTexture == (GLint)mTarget.getTexture()); + CHECK(state.mLevel == 1); + CHECK(state.mAccess == GL_WRITE_ONLY); + CHECK(state.mFormat == GL_R32F); + } state = unitState(4); - UNSCOPED_INFO("the other texture put back"); - CHECK(state.mTexture == (GLint)mOther.getTexture()); - UNSCOPED_INFO("its access put back"); - CHECK(state.mAccess == GL_READ_ONLY); - UNSCOPED_INFO("its format put back"); - CHECK(state.mFormat == GL_RGBA16F); + { + INFO("unit 4 put back"); + CHECK(state.mTexture == (GLint)mOther.getTexture()); + CHECK(state.mAccess == GL_READ_ONLY); + CHECK(state.mFormat == GL_RGBA16F); + } - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -255,12 +232,10 @@ namespace // the viewer's way, through LLImageGL's deferred delete, reaches it. TEST_CASE_METHOD(alimageslot_data, "alimageslot_gl: forgetTextures drops only the units that held a deleted name", "[alimageslot_gl]") { - setUp(); + require_targets(); ALImageSlot* slot = gGL.getImageSlot(1); ALImageSlot* kept = gGL.getImageSlot(5); - UNSCOPED_INFO("bound"); CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); - UNSCOPED_INFO("other bound"); CHECK(kept->bind(&mOther, 0, ALImageSlot::READ_ONLY)); const U32 name = mTarget.getTexture(); @@ -271,7 +246,6 @@ namespace CHECK(kept->getCurrTexture() == mOther.getTexture()); const U32 binds = ALImageSlot::sImageBinds; - UNSCOPED_INFO("bound again"); CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); UNSCOPED_INFO("the forgotten name is bound, not skipped"); CHECK(ALImageSlot::sImageBinds == binds + 1); @@ -289,10 +263,8 @@ namespace CHECK(unitState(5).mTexture == 0); UNSCOPED_INFO("the live one stays"); CHECK(slot->getCurrTexture() == name); - UNSCOPED_INFO("the deleted name is gone"); CHECK(glIsTexture(other) == GL_FALSE); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -301,59 +273,45 @@ namespace // with a linear format, and a volume, bind. TEST_CASE_METHOD(alimageslot_data, "alimageslot_gl: sRGB and depth textures and units past the slots are refused", "[alimageslot_gl]") { - setUp(); + require_targets(); ALImageSlot* slot = gGL.getImageSlot(6); - UNSCOPED_INFO("bound"); CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); const U32 binds = ALImageSlot::sImageBinds; LLPointer raw = new LLImageRaw(4, 4, 4); std::memset(raw->getData(), 0x80, 4 * 4 * 4); LLPointer srgb = new LLImageGL(raw, false); - UNSCOPED_INFO("an image's texture is sRGB"); CHECK(srgb->getStorageInternalFormat() == (S32)GL_SRGB8_ALPHA8); - UNSCOPED_INFO("sRGB refused"); CHECK_FALSE(slot->bind(srgb.get(), ALImageSlot::READ_ONLY)); LLRenderTarget depth; - UNSCOPED_INFO("depth target allocates"); REQUIRE(depth.allocate(SIZE, SIZE, 0, true)); - UNSCOPED_INFO("depth refused"); CHECK_FALSE(slot->bindManual(depth.getDepth(), 0, false, 0, ALImageSlot::READ_ONLY, GL_DEPTH_COMPONENT24)); depth.release(); - UNSCOPED_INFO("past the slots refused"); CHECK_FALSE(gGL.getImageSlot(AL_NUM_IMAGE_SLOTS)->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); UNSCOPED_INFO("nothing issued"); CHECK(ALImageSlot::sImageBinds == binds); UNSCOPED_INFO("the unit unchanged"); CHECK(unitState(6).mTexture == (GLint)mTarget.getTexture()); - UNSCOPED_INFO("and the slot"); + UNSCOPED_INFO("nor the slot"); CHECK(slot->getCurrTexture() == mTarget.getTexture()); LLPointer linear = new LLImageGL(4, 4, 4, false); linear->setExplicitFormat(GL_RGBA8, GL_RGBA); - UNSCOPED_INFO("linear image created"); - CHECK(linear->createGLTexture(0, raw.get())); - UNSCOPED_INFO("linear bound"); + REQUIRE(linear->createGLTexture(0, raw.get())); CHECK(slot->bind(linear.get(), ALImageSlot::READ_ONLY)); UnitState state = unitState(6); - UNSCOPED_INFO("GL holds the image"); CHECK(state.mTexture == (GLint)linear->getTexName()); - UNSCOPED_INFO("at RGBA8"); CHECK(state.mFormat == GL_RGBA8); - UNSCOPED_INFO("one layer"); CHECK(state.mLayered == GL_FALSE); LLPointer volume = new ALTexture3D(); - UNSCOPED_INFO("volume allocates"); - CHECK(volume->allocate(4, 4, 4, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, nullptr)); - UNSCOPED_INFO("volume bound"); + REQUIRE(volume->allocate(4, 4, 4, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, nullptr)); CHECK(slot->bind(volume.get(), ALImageSlot::WRITE_ONLY)); state = unitState(6); - UNSCOPED_INFO("GL holds the volume"); CHECK(state.mTexture == (GLint)volume->getGLName()); UNSCOPED_INFO("every slice"); CHECK(state.mLayered == GL_TRUE); @@ -361,7 +319,6 @@ namespace CHECK(state.mFormat == GL_RGBA16F); slot->unbind(); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -370,13 +327,13 @@ namespace // did, and both come back changed exactly, past what a float could carry. TEST_CASE_METHOD(alimageslot_data, "alimageslot_gl: a kernel changes R32UI target and image texels exactly", "[alimageslot_gl]") { - setUp(); + require_targets(); constexpr U32 W = 13; constexpr U32 H = 7; const GLuint cs = ll_test::compileTestShader(GL_COMPUTE_SHADER, kUintKernel); UNSCOPED_INFO("kernel compiles"); - CHECK(cs != 0); + REQUIRE(cs != 0); mKernel = glCreateProgram(); glAttachShader(mKernel, cs); glLinkProgram(mKernel); @@ -384,10 +341,9 @@ namespace GLint linked = GL_FALSE; glGetProgramiv(mKernel, GL_LINK_STATUS, &linked); UNSCOPED_INFO("kernel links"); - CHECK(linked == GL_TRUE); + REQUIRE(linked == GL_TRUE); - UNSCOPED_INFO("R32UI target allocates"); - CHECK(mUint.allocate(W, H, GL_R32UI)); + REQUIRE(mUint.allocate(W, H, GL_R32UI)); mUint.bindTarget(); gGL.setClearColor(LLColor4(5.f, 0.f, 0.f, 0.f)); mUint.clear(); @@ -395,9 +351,7 @@ namespace ALImageSlot* slot = gGL.getImageSlot(0); glUseProgram(mKernel); - UNSCOPED_INFO("target bound"); CHECK(slot->bind(&mUint, 0, ALImageSlot::READ_WRITE)); - UNSCOPED_INFO("as R32UI"); CHECK(unitState(0).mFormat == GL_R32UI); glDispatchCompute((W + 7) / 8, (H + 7) / 8, 1); gGL.memoryBarrier(LLRender::BARRIER_TEXTURE_UPDATE); @@ -410,11 +364,8 @@ namespace } LLPointer image = new LLImageGL(W, H, 1, /*usemipmaps=*/false); image->setExplicitFormat(GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT); - UNSCOPED_INFO("R32UI image created"); - CHECK(image->createGLTexture(0, reinterpret_cast(values.data()))); - UNSCOPED_INFO("image bound"); + REQUIRE(image->createGLTexture(0, reinterpret_cast(values.data()))); CHECK(slot->bind(image.get(), ALImageSlot::READ_WRITE)); - UNSCOPED_INFO("as R32UI too"); CHECK(unitState(0).mFormat == GL_R32UI); glDispatchCompute((W + 7) / 8, (H + 7) / 8, 1); gGL.memoryBarrier(LLRender::BARRIER_TEXTURE_UPDATE); @@ -426,15 +377,13 @@ namespace { for (U32 x = 0; x < W; ++x) { - const std::string at = " at " + std::to_string(x) + "," + std::to_string(y); + CAPTURE(x, y); const size_t i = (size_t)y * W + x; - UNSCOPED_INFO("target" + at); REQUIRE(target[i] == 5u * 3u + x + y * 100u); - UNSCOPED_INFO("image" + at); + UNSCOPED_INFO("the image"); REQUIRE(held[i] == values[i] * 3u + x + y * 100u); } } - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } } diff --git a/indra/llrender/tests/alinfiniteprojection_gl_test.cpp b/indra/llrender/tests/alinfiniteprojection_gl_test.cpp index af9e0aada9..5ed6e330eb 100644 --- a/indra/llrender/tests/alinfiniteprojection_gl_test.cpp +++ b/indra/llrender/tests/alinfiniteprojection_gl_test.cpp @@ -145,7 +145,6 @@ namespace glDeleteShader(fs); GLint linked = GL_FALSE; glGetProgramiv(mProgram, GL_LINK_STATUS, &linked); - UNSCOPED_INFO("program links"); REQUIRE(linked == GL_TRUE); glGenVertexArrays(1, &mVAO); @@ -160,7 +159,6 @@ namespace glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, mDepth, 0); glDrawBuffer(GL_NONE); glReadBuffer(GL_NONE); - UNSCOPED_INFO("complete"); REQUIRE((U32)glCheckFramebufferStatus(GL_FRAMEBUFFER) == (U32)GL_FRAMEBUFFER_COMPLETE); glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE); @@ -206,20 +204,17 @@ namespace TEST_CASE_METHOD(alinfiniteprojection_data, "alinfiniteprojection_gl: the infinite matrix stores near over distance and its far end is floored", "[alinfiniteprojection_gl]") { const LLMatrix4a proj = infinite(); - UNSCOPED_INFO("infinite"); CHECK(al_projection_is_infinite(proj)); UNSCOPED_INFO("finite reversed is not"); CHECK_FALSE(al_projection_is_infinite(al_perspective(FOV, 1.f, NEAR_PLANE, 1024.f))); - UNSCOPED_INFO("ortho is not"); CHECK_FALSE(al_projection_is_infinite(al_ortho(-1.f, 1.f, -1.f, 1.f, 0.f, 10.f))); for (F32 dist : DISTANCES) { + CAPTURE(dist); LLVector4a clip; proj.transform4(LLVector4a(0.f, 0.f, -dist, 1.f), clip); - UNSCOPED_INFO("clip z is the near plane"); REQUIRE(clip[2] == NEAR_PLANE); - UNSCOPED_INFO("clip w is the distance"); REQUIRE(clip[3] == dist); } @@ -229,7 +224,6 @@ namespace inv.transform4(LLVector4a(0.f, 0.f, 0.f, 1.f), far_point); UNSCOPED_INFO("cleared depth unprojects to w = 0"); CHECK(fabsf(far_point[3]) < 1e-12f); - UNSCOPED_INFO("floored to MAX_RECONSTRUCT_DISTANCE"); CHECK_THAT(reconstruct(inv, 0.f), ll_test::WithinBits(MAX_RECONSTRUCT_DISTANCE, 8)); // The floor leaves a finite projection alone: its cleared depth still reconstructs at its far plane. @@ -237,18 +231,13 @@ namespace inv_finite.invert(); LLVector4a raw; inv_finite.transform4(LLVector4a(0.f, 0.f, 0.f, 1.f), raw); - UNSCOPED_INFO("finite far untouched by the floor"); CHECK(reconstruct(inv_finite, 0.f) == -raw[2] / raw[3]); - UNSCOPED_INFO("finite far is about 1024 m"); CHECK(fabsf(-raw[2] / raw[3] - 1024.f) < 1.f); LLRender::sReverseZ = false; const LLMatrix4a forward = al_perspective(FOV, 1.f, NEAR_PLANE, std::numeric_limits::infinity()); - { - INFO("forward falls back to a finite plane"); - CHECK_FALSE(al_projection_is_infinite(forward)); - CHECK(std::isfinite(forward.getColumn<2>()[2])); - } + CHECK_FALSE(al_projection_is_infinite(forward)); + CHECK(std::isfinite(forward.getColumn<2>()[2])); LLRender::sReverseZ = true; } @@ -265,16 +254,15 @@ namespace for (size_t i = 0; i < std::size(DISTANCES); ++i) { const F32 dist = DISTANCES[i]; - const std::string at = std::to_string((S32)dist) + " m"; + CAPTURE(dist); glClear(GL_DEPTH_BUFFER_BIT); draw(proj, dist); const F32 d = centreDepth(); - UNSCOPED_INFO("drawn at " + at); + UNSCOPED_INFO("drawn"); REQUIRE(d > 0.f); - UNSCOPED_INFO("stored depth at " + at); REQUIRE(fabsf(d - oracle_depth[i]) <= oracle_depth[i] * 1e-6f); const F64 err = fabs((F64)reconstruct(inv, d) - dist); - UNSCOPED_INFO("round trip at " + at + ": " + std::to_string(err * 1000.0) + " mm"); + UNSCOPED_INFO("round trip error " << err * 1000.0 << " mm"); REQUIRE(err <= bound(dist, d)); } @@ -283,7 +271,6 @@ namespace draw(proj, 1.0e6f); UNSCOPED_INFO("1000 km is not clipped"); CHECK(centreDepth() > 0.f); - UNSCOPED_INFO("no GL error"); CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } @@ -295,7 +282,7 @@ namespace for (F32 dist : DISTANCES) { - const std::string at = std::to_string((S32)dist) + " m"; + CAPTURE(dist); const F32 gap = (F32)(4.0 * depthStep(NEAR_PLANE / dist)) + (std::nextafter(dist, 2.f * dist) - dist); const F32 nearer = dist - gap; @@ -304,16 +291,15 @@ namespace const F32 d_far = centreDepth(); draw(proj, nearer); const F32 d_near = centreDepth(); - UNSCOPED_INFO("nearer drawn second wins at " + at); + UNSCOPED_INFO("nearer drawn second wins"); REQUIRE(d_near > d_far); glClear(GL_DEPTH_BUFFER_BIT); draw(proj, nearer); draw(proj, dist); - UNSCOPED_INFO("farther drawn second loses at " + at); + UNSCOPED_INFO("farther drawn second loses"); REQUIRE(centreDepth() == d_near); } - UNSCOPED_INFO("no GL error"); CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } @@ -327,7 +313,7 @@ namespace LLRender::sReverseZ = true; for (S32 i = 0; i < 16; ++i) { - UNSCOPED_INFO("same as the old fixed plane"); + CAPTURE(i); REQUIRE(forward.getF32ptr()[i] == legacy.getF32ptr()[i]); } LLVector4a clip; @@ -370,17 +356,16 @@ namespace for (F32 dist : { 1000.f, 100000.f, MAX_RECONSTRUCT_DISTANCE }) { - const std::string at = std::to_string((S32)dist) + " m"; + CAPTURE(dist); glClear(GL_DEPTH_BUFFER_BIT); draw(proj, dist); - UNSCOPED_INFO("geometry at " + at + " stores more than the far plane"); + UNSCOPED_INFO("geometry stores more than the far plane"); REQUIRE(centreDepth() > 0.f); - UNSCOPED_INFO("the sky stays behind geometry at " + at); + UNSCOPED_INFO("the sky stays behind geometry"); REQUIRE(sky_samples() == (GLuint)0); } glDeleteQueries(1, &query); - UNSCOPED_INFO("no GL error"); CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } @@ -414,10 +399,10 @@ namespace for (F32 dist : { 1.f, 1000.f, 100000.f, MAX_RECONSTRUCT_DISTANCE }) { - const std::string at = std::to_string((S32)dist) + " m"; + CAPTURE(dist); glClear(GL_DEPTH_BUFFER_BIT); draw(proj, dist); - UNSCOPED_INFO("haze over all of the geometry at " + at); + UNSCOPED_INFO("haze over all of the geometry"); REQUIRE(haze_samples() == (GLuint)(SIZE * SIZE)); } @@ -430,7 +415,6 @@ namespace CHECK(haze_samples() == (GLuint)(SIZE * SIZE / 2)); glDeleteQueries(1, &query); - UNSCOPED_INFO("no GL error"); CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } @@ -465,12 +449,12 @@ namespace for (F32 dist : { 1.f, 1000.f, 100000.f, MAX_RECONSTRUCT_DISTANCE }) { - const std::string at = std::to_string((S32)dist) + " m"; + CAPTURE(dist); glClear(GL_DEPTH_BUFFER_BIT); draw(proj, dist); - UNSCOPED_INFO("all lit over geometry at " + at); + UNSCOPED_INFO("all lit over geometry"); REQUIRE(samples(GL_GREATER) == (GLuint)(SIZE * SIZE)); - UNSCOPED_INFO("no sky over geometry at " + at); + UNSCOPED_INFO("no sky over geometry"); REQUIRE(samples(GL_EQUAL) == (GLuint)0); } @@ -487,7 +471,6 @@ namespace CHECK(lit + sky == (GLuint)(SIZE * SIZE)); glDeleteQueries(1, &query); - UNSCOPED_INFO("no GL error"); CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } } diff --git a/indra/llrender/tests/alinstanceddraw_gl_test.cpp b/indra/llrender/tests/alinstanceddraw_gl_test.cpp index c7c0a0b886..7040910af5 100644 --- a/indra/llrender/tests/alinstanceddraw_gl_test.cpp +++ b/indra/llrender/tests/alinstanceddraw_gl_test.cpp @@ -107,12 +107,10 @@ namespace REQUIRE(mProgram.mapAttributes()); glDeleteShader(vs); glDeleteShader(fs); - UNSCOPED_INFO("grid program maps"); REQUIRE(mProgram.mapUniforms()); UNSCOPED_INFO("the grid program has no vertex inputs"); REQUIRE(mProgram.mAttributeMask == 0U); - UNSCOPED_INFO("target allocates"); REQUIRE(mTarget.allocate(W, H, GL_RGBA8)); mTarget.bindTarget(); gGL.setClearColor(LLColor4::black); @@ -161,7 +159,6 @@ namespace setUp(); gGL.loadMatrix(grid(0.f)); gGL.drawArraysInstanced(LLRender::TRIANGLE_STRIP, 0, 4, 3); - UNSCOPED_INFO("cells covered"); CHECK(covered() == 0x7U); } diff --git a/indra/llrender/tests/alrenderstate_gl_test.cpp b/indra/llrender/tests/alrenderstate_gl_test.cpp index 460d4c6f19..6846753e9e 100644 --- a/indra/llrender/tests/alrenderstate_gl_test.cpp +++ b/indra/llrender/tests/alrenderstate_gl_test.cpp @@ -82,7 +82,6 @@ namespace void setUp() { - UNSCOPED_INFO("target allocates"); REQUIRE(mTarget.allocate(SIZE, SIZE, GL_RGBA8)); } @@ -97,17 +96,13 @@ namespace setUp(); mTarget.bindTarget(); gGL.setClearColor(LLColor4::red); - UNSCOPED_INFO("GL holds the cached colour"); CHECK(glClearValue() == gGL.getClearColor()); mTarget.clear(); - UNSCOPED_INFO("cleared red"); CHECK(pixel(1, 1) == RED); gGL.setClearColor(LLColor4::green); mTarget.clear(); - UNSCOPED_INFO("cleared green"); CHECK(pixel(1, 1) == GREEN); mTarget.flush(); - UNSCOPED_INFO("no GL error"); CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } @@ -126,18 +121,13 @@ namespace CHECK(glClearValue() == LLColor4::red); gGL.refreshState(); - UNSCOPED_INFO("refreshState restores the clear colour"); CHECK(glClearValue() == LLColor4::green); S32 box[4]; glScissorBox(box); - { - INFO("refreshState restores the scissor box"); - CHECK(box[0] == 1); - CHECK(box[1] == 1); - CHECK(box[2] == 2); - CHECK(box[3] == 2); - } - UNSCOPED_INFO("no GL error"); + CHECK(box[0] == 1); + CHECK(box[1] == 1); + CHECK(box[2] == 2); + CHECK(box[3] == 2); CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } @@ -198,11 +188,10 @@ namespace CHECK(box[3] == (S32)SIZE); } mTarget.flush(); - UNSCOPED_INFO("no GL error"); CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } - // And the patch size, on a context with tessellation to have one. + // refreshState puts back the patch size too, on a context with tessellation to have one. TEST_CASE_METHOD(alrenderstate_data, "alrenderstate_gl: refreshState puts back the patch size gGL holds", "[alrenderstate_gl]") { if (!glPatchParameteri) @@ -232,9 +221,7 @@ namespace gGL.refreshState(); glGetIntegerv(GL_PATCH_VERTICES, &patch); - UNSCOPED_INFO("refreshState restores the patch size"); CHECK((S32)patch == 4); - UNSCOPED_INFO("no GL error"); CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } } diff --git a/indra/llrender/tests/altessellation_gl_test.cpp b/indra/llrender/tests/altessellation_gl_test.cpp index 74978e9487..db3f820e0a 100644 --- a/indra/llrender/tests/altessellation_gl_test.cpp +++ b/indra/llrender/tests/altessellation_gl_test.cpp @@ -276,9 +276,7 @@ namespace // vertex and fragment shader. Everything below assumes this. TEST_CASE_METHOD(altessellation_data, "ALTessellation: the context compiles and links a four-stage tessellation program", "[ALTessellation]") { - UNSCOPED_INFO("all four stages compiled"); CHECK(mCompiled); - UNSCOPED_INFO("four-stage program linked"); CHECK(mLinked); UNSCOPED_INFO("outer levels uniform found"); CHECK(mOuter >= 0); @@ -290,8 +288,7 @@ namespace // the whole framebuffer, and back-face culling keeps them. TEST_CASE_METHOD(altessellation_data, "ALTessellation: at level 1 a counter-clockwise patch covers the whole framebuffer", "[ALTessellation]") { - UNSCOPED_INFO("program linked"); - CHECK(mLinked); + REQUIRE(mLinked); setLevels(1, 1, 1, 1, 1, 1); const std::vector px = draw(kCCW); UNSCOPED_INFO("bottom-left covered"); @@ -310,8 +307,7 @@ namespace // x and v along y is the winding the pipeline's cull expects. TEST_CASE_METHOD(altessellation_data, "ALTessellation: the same corners clockwise are culled as back faces", "[ALTessellation]") { - UNSCOPED_INFO("program linked"); - CHECK(mLinked); + REQUIRE(mLinked); setLevels(1, 1, 1, 1, 1, 1); const std::vector px = draw(kCW); UNSCOPED_INFO("bottom-left culled"); @@ -326,17 +322,16 @@ namespace // no cracks between the pieces, at integer and at fractional levels. TEST_CASE_METHOD(altessellation_data, "ALTessellation: subdivided with fractional spacing the patch still covers every pixel", "[ALTessellation]") { - UNSCOPED_INFO("program linked"); - CHECK(mLinked); + REQUIRE(mLinked); for (const F32 level : { 4.f, 2.5f, 7.25f }) { + CAPTURE(level); setLevels(level, level, level, level, level, level); const std::vector px = draw(kCCW); size_t uncovered = 0; for (S32 y = 0; y < H; ++y) for (S32 x = 0; x < W; ++x) if (!white(px, x, y)) ++uncovered; - UNSCOPED_INFO("every pixel covered at level " + std::to_string(level)); REQUIRE(uncovered == size_t(0)); } } @@ -347,16 +342,13 @@ namespace // level above 1 whose segments are all equal and whose count is exact. TEST_CASE_METHOD(altessellation_data, "ALTessellation: the tessellator emits two triangles at level 1 and eighteen at level 3", "[ALTessellation]") { - UNSCOPED_INFO("program linked"); - CHECK(mLinked); + REQUIRE(mLinked); GLuint triangles = 0; setLevels(1, 1, 1, 1, 1, 1); capture(kCCW, triangles); - UNSCOPED_INFO("level 1 is two triangles"); CHECK(triangles == GLuint(2)); setLevels(3, 3, 3, 3, 3, 3); capture(kCCW, triangles); - UNSCOPED_INFO("level 3 is eighteen triangles"); CHECK(triangles == GLuint(18)); } @@ -366,8 +358,7 @@ namespace // (y = -1), [2] u = 1 (x = +1), [3] v = 1 (y = +1). TEST_CASE_METHOD(altessellation_data, "ALTessellation: each outer level splits only its own edge of the patch", "[ALTessellation]") { - UNSCOPED_INFO("program linked"); - CHECK(mLinked); + REQUIRE(mLinked); // Edge i of the patch, as outer[i] should see it. struct Edge { int axis; F32 value; const char* name; }; const Edge edges[4] = { @@ -385,8 +376,9 @@ namespace const std::vector verts = capture(kCCW, triangles); for (int i = 0; i < 4; ++i) { + CAPTURE(i); const size_t expected = i == raised ? 4 : 2; - UNSCOPED_INFO(std::string(edges[raised].name) + (i == raised ? "" : ": other side unsplit")); + UNSCOPED_INFO(edges[raised].name << (i == raised ? "" : ": other side unsplit")); REQUIRE(distinctOn(verts, edges[i].axis, edges[i].value) == expected); } } @@ -396,8 +388,7 @@ namespace // OpenGL driver aborts when GL_PRIMITIVES_GENERATED is active here. TEST_CASE_METHOD(altessellation_data, "ALTessellation: a profiled tessellated draw still renders", "[ALTessellation]") { - UNSCOPED_INFO("program linked"); - CHECK(mLinked); + REQUIRE(mLinked); // Viewer programs do not capture transform feedback. Declaring a // captured varying takes a different Apple driver path and hides the crash. glTransformFeedbackVaryings(mProgram, 0, nullptr, GL_INTERLEAVED_ATTRIBS); @@ -405,7 +396,7 @@ namespace GLint linked = GL_FALSE; glGetProgramiv(mProgram, GL_LINK_STATUS, &linked); UNSCOPED_INFO("program linked without transform feedback"); - CHECK(linked == GL_TRUE); + REQUIRE(linked == GL_TRUE); mOuter = glGetUniformLocation(mProgram, "outer"); mInner = glGetUniformLocation(mProgram, "inner"); setLevels(3, 3, 3, 3, 3, 3); @@ -425,7 +416,6 @@ namespace CHECK(white(px, W / 2, H / 2)); UNSCOPED_INFO("sample query still counts fragments"); CHECK(profile.mSamplesDrawn > 0); - UNSCOPED_INFO("profiling leaves no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } } diff --git a/indra/llrender/tests/alwindowsdlheadless_test.cpp b/indra/llrender/tests/alwindowsdlheadless_test.cpp index 5a07de583a..41a57d81c2 100644 --- a/indra/llrender/tests/alwindowsdlheadless_test.cpp +++ b/indra/llrender/tests/alwindowsdlheadless_test.cpp @@ -57,9 +57,7 @@ namespace TEST_CASE_METHOD(alwindowsdlheadless_data, "ALWindowSDLHeadless: the fixture's window is the hidden backend at the size asked for", "[ALWindowSDLHeadless]") { LLWindow* window = gl().window(); - UNSCOPED_INFO("a window"); REQUIRE(window != nullptr); - UNSCOPED_INFO("the hidden backend was asked for"); CHECK(LLWindowManager::getBackend() == ALWindowBackend::Hidden); // createWindow would have returned null on an invalid one; what is // left to see is the SDL window behind it. @@ -67,11 +65,8 @@ namespace REQUIRE(window->getPlatformWindow() != nullptr); LLCoordWindow size; - UNSCOPED_INFO("the size can be read"); REQUIRE(window->getSize(&size)); - UNSCOPED_INFO("width as asked"); CHECK(size.mX == ll_test::HeadlessGL::WIDTH); - UNSCOPED_INFO("height as asked"); CHECK(size.mY == ll_test::HeadlessGL::HEIGHT); } @@ -84,7 +79,6 @@ namespace LLWindow* window = gl().window(); void* shared = window->createSharedContext(); - UNSCOPED_INFO("a shared context was made"); REQUIRE(shared != nullptr); GLuint name = 0; @@ -107,7 +101,6 @@ namespace }); worker.join(); - UNSCOPED_INFO("the worker's GL calls raised no error"); CHECK(worker_error == (GLenum)GL_NO_ERROR); UNSCOPED_INFO("the worker made a texture"); CHECK(name != 0); @@ -124,18 +117,16 @@ namespace glBindTexture(GL_TEXTURE_2D, prior); UNSCOPED_INFO("with the storage the worker gave it"); CHECK(width == 1); - UNSCOPED_INFO("and no error on this side"); + UNSCOPED_INFO("no error on the main context"); CHECK(glGetError() == (GLenum)GL_NO_ERROR); glDeleteTextures(1, &name); } - // A shared context nobody bound is still a shared context to destroy. TEST_CASE_METHOD(alwindowsdlheadless_data, "ALWindowSDLHeadless: a shared context nobody bound can still be destroyed", "[ALWindowSDLHeadless]") { LLWindow* window = gl().window(); void* shared = window->createSharedContext(); - UNSCOPED_INFO("a shared context was made"); REQUIRE(shared != nullptr); window->destroySharedContext(shared); UNSCOPED_INFO("the main context is untouched"); diff --git a/indra/llrender/tests/llfontbitmapcache_gl_test.cpp b/indra/llrender/tests/llfontbitmapcache_gl_test.cpp index 3302f5b82c..ccabdaa6f3 100644 --- a/indra/llrender/tests/llfontbitmapcache_gl_test.cpp +++ b/indra/llrender/tests/llfontbitmapcache_gl_test.cpp @@ -57,13 +57,10 @@ namespace c.init(2, 2); S32 px = 0, py = 0; U32 sheet = 0; - UNSCOPED_INFO("nextOpenPos succeeded"); CHECK(c.nextOpenPos(/*width=*/8, /*height=*/8, px, py, EFontGlyphType::Grayscale, sheet)); LLImageRaw* raw = c.getImageRaw(EFontGlyphType::Grayscale, sheet); - UNSCOPED_INFO("raw image present"); REQUIRE(raw != nullptr); - UNSCOPED_INFO("Grayscale atlas has 2 components"); CHECK((S32)raw->getComponents() == 2); } @@ -75,13 +72,10 @@ namespace c.init(2, 2); S32 px = 0, py = 0; U32 sheet = 0; - UNSCOPED_INFO("nextOpenPos succeeded"); CHECK(c.nextOpenPos(8, 8, px, py, EFontGlyphType::Color, sheet)); LLImageRaw* raw = c.getImageRaw(EFontGlyphType::Color, sheet); - UNSCOPED_INFO("raw image present"); REQUIRE(raw != nullptr); - UNSCOPED_INFO("Color atlas has 4 components"); CHECK((S32)raw->getComponents() == 4); } @@ -95,13 +89,9 @@ namespace c.init(2, 2); S32 px = -1, py = -1; U32 sheet = 99; - UNSCOPED_INFO("first Grayscale alloc succeeded"); CHECK(c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, sheet)); - UNSCOPED_INFO("first Grayscale posX == 4"); CHECK(px == 4); - UNSCOPED_INFO("first Grayscale posY == 4"); CHECK(py == 4); - UNSCOPED_INFO("first Grayscale sheet index == 0"); CHECK((S32)sheet == 0); // Repeat for Color — a fresh cache, fresh allocation. The bug @@ -109,11 +99,8 @@ namespace LLFontBitmapCache c2; c2.init(2, 2); S32 px2 = -1, py2 = -1; U32 sheet2 = 99; - UNSCOPED_INFO("first Color alloc succeeded"); CHECK(c2.nextOpenPos(8, 8, px2, py2, EFontGlyphType::Color, sheet2)); - UNSCOPED_INFO("first Color posX == 4"); CHECK(px2 == 4); - UNSCOPED_INFO("first Color posY == 4"); CHECK(py2 == 4); } @@ -127,16 +114,14 @@ namespace S32 px = 0, py = 0; U32 sheet = 0; c.nextOpenPos(10, 8, px, py, EFontGlyphType::Grayscale, sheet); - UNSCOPED_INFO("first posX"); CHECK(px == 4); - UNSCOPED_INFO("first posY"); CHECK(py == 4); c.nextOpenPos(10, 8, px, py, EFontGlyphType::Grayscale, sheet); // Expected: 4 (initial border) + 10 (first width) + 4 (border) - UNSCOPED_INFO("second posX advances by width+4"); + UNSCOPED_INFO("the second allocation advances by width + 4"); CHECK(px == 18); - UNSCOPED_INFO("second posY stays in same row"); + UNSCOPED_INFO("in the same row"); CHECK(py == 4); } @@ -149,22 +134,21 @@ namespace // (4 + 50 + 4 + 50 + 4 = 112 ≤ 128); third forces wrap. LLFontBitmapCache c; c.init(2, 2); - UNSCOPED_INFO("sheet width is 128"); CHECK(c.getBitmapWidth() == 128); S32 px = 0, py = 0; U32 sheet = 0; c.nextOpenPos(50, 30, px, py, EFontGlyphType::Grayscale, sheet); // row 0, col 0 c.nextOpenPos(50, 20, px, py, EFontGlyphType::Grayscale, sheet); // row 0, col 1 - UNSCOPED_INFO("second alloc still in row 0"); + UNSCOPED_INFO("the second allocation is still in row 0"); CHECK(py == 4); c.nextOpenPos(50, 8, px, py, EFontGlyphType::Grayscale, sheet); // wraps to row 1 - UNSCOPED_INFO("third alloc wraps posX to 4"); + UNSCOPED_INFO("the third allocation wraps"); CHECK(px == 4); // Tallest in row 0 was 30, so row 1 starts at 4 + 30 + 4 = 38. - UNSCOPED_INFO("third alloc posY uses tallest glyph in prior row"); + UNSCOPED_INFO("below the tallest glyph of row 0"); CHECK(py == 38); - UNSCOPED_INFO("still on sheet 0 (row wrap, not sheet rollover)"); + UNSCOPED_INFO("a row wrap and not a new sheet"); CHECK((S32)sheet == 0); } @@ -175,9 +159,7 @@ namespace { LLFontBitmapCache c; c.init(2, 2); - UNSCOPED_INFO("sheet width is 128"); CHECK(c.getBitmapWidth() == 128); - UNSCOPED_INFO("sheet height is 128"); CHECK(c.getBitmapHeight() == 128); // Glyphs 50 wide, 30 tall. 2 per row (positions 4 and 58), @@ -188,23 +170,22 @@ namespace { c.nextOpenPos(50, 30, px, py, EFontGlyphType::Grayscale, sheet); } - UNSCOPED_INFO("six allocs of 50x30 still on sheet 0"); + UNSCOPED_INFO("six allocations fill three rows of sheet 0"); CHECK((S32)sheet == 0); - UNSCOPED_INFO("one sheet so far"); CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); // 7th glyph: Y advance to 4+30+4+30+4+30+4=106, bottom would // be 106+30+4=140 > 128 → new sheet. Either the row-wrap path // (line 125) or the vertical-fit recheck (line 144) fires. c.nextOpenPos(50, 30, px, py, EFontGlyphType::Grayscale, sheet); - UNSCOPED_INFO("7th alloc moved to new sheet"); + UNSCOPED_INFO("the seventh allocation goes to a new sheet"); CHECK((S32)sheet == 1); - UNSCOPED_INFO("two sheets allocated"); CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 2u); - UNSCOPED_INFO("new sheet starts at posX=4"); - CHECK(px == 4); - UNSCOPED_INFO("new sheet starts at posY=4"); - CHECK(py == 4); + { + INFO("the new sheet's first position"); + CHECK(px == 4); + CHECK(py == 4); + } } // Per-type isolation: a Grayscale allocation must not move the @@ -222,15 +203,13 @@ namespace // Color must still be at the (4, 4) start. S32 cpx = 0, cpy = 0; U32 csheet = 99; c.nextOpenPos(8, 8, cpx, cpy, EFontGlyphType::Color, csheet); - UNSCOPED_INFO("Color pen unaffected by Grayscale activity (x)"); - CHECK(cpx == 4); - UNSCOPED_INFO("Color pen unaffected by Grayscale activity (y)"); - CHECK(cpy == 4); - UNSCOPED_INFO("Color sheet index starts at 0"); + { + INFO("the Color pen after two Grayscale allocations"); + CHECK(cpx == 4); + CHECK(cpy == 4); + } CHECK((S32)csheet == 0); - UNSCOPED_INFO("Grayscale + Color each have one sheet"); CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); - UNSCOPED_INFO("Color has its own sheet vector"); CHECK(c.getNumBitmaps(EFontGlyphType::Color) == 1u); } @@ -247,17 +226,14 @@ namespace S32 px = 0, py = 0; U32 sheet = 0; c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, sheet); const S32 g1 = c.getCacheGeneration(); - UNSCOPED_INFO("generation bumped by nextOpenPos"); CHECK(g1 > g0); c.releaseSheet(EFontGlyphType::Grayscale, sheet); const S32 g2 = c.getCacheGeneration(); - UNSCOPED_INFO("generation bumped by releaseSheet"); CHECK(g2 > g1); c.reset(); const S32 g3 = c.getCacheGeneration(); - UNSCOPED_INFO("generation bumped by reset"); CHECK(g3 > g2); } @@ -271,7 +247,6 @@ namespace S32 px = 0, py = 0; U32 sheet = 0; c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, sheet); - UNSCOPED_INFO("sheet not yet released"); CHECK_FALSE(c.isSheetReleased(EFontGlyphType::Grayscale, sheet)); // Don't assert getSheetLastUsedTime is > 0 here — it samples // LLFrameTimer::getTotalSeconds() and the test environment may @@ -282,14 +257,11 @@ namespace c.releaseSheet(EFontGlyphType::Grayscale, sheet); - UNSCOPED_INFO("isSheetReleased flips true after release"); CHECK(c.isSheetReleased(EFontGlyphType::Grayscale, sheet)); - UNSCOPED_INFO("released sheet getImageRaw returns null"); - REQUIRE(c.getImageRaw(EFontGlyphType::Grayscale, sheet) == - (LLImageRaw*)nullptr); - UNSCOPED_INFO("released sheet last-used time cleared to 0"); + CHECK(c.getImageRaw(EFontGlyphType::Grayscale, sheet) == nullptr); + UNSCOPED_INFO("a released sheet's last-used time is cleared"); CHECK(c.getSheetLastUsedTime(EFontGlyphType::Grayscale, sheet) == 0.0); - UNSCOPED_INFO("getNumBitmaps preserves slot count after release"); + UNSCOPED_INFO("the slot is kept"); CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); } @@ -309,17 +281,15 @@ namespace c.releaseSheet(EFontGlyphType::Grayscale, sheet); S32 px2 = 0, py2 = 0; U32 sheet2 = 99; - UNSCOPED_INFO("nextOpenPos succeeds after active sheet release"); CHECK(c.nextOpenPos(8, 8, px2, py2, EFontGlyphType::Grayscale, sheet2)); - UNSCOPED_INFO("released slot 0 is recycled for the new sheet"); + UNSCOPED_INFO("the released slot is recycled"); CHECK((S32)sheet2 == 0); - UNSCOPED_INFO("new sheet starts at posX=4"); - CHECK(px2 == 4); - UNSCOPED_INFO("new sheet starts at posY=4"); - CHECK(py2 == 4); - UNSCOPED_INFO("recycled slot 0 is live again"); + { + INFO("the new sheet's first position"); + CHECK(px2 == 4); + CHECK(py2 == 4); + } CHECK_FALSE(c.isSheetReleased(EFontGlyphType::Grayscale, 0)); - UNSCOPED_INFO("slot vector did not grow"); CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); } @@ -333,22 +303,18 @@ namespace S32 px = 0, py = 0; U32 sheet = 0; c.nextOpenPos(40, 20, px, py, EFontGlyphType::Grayscale, sheet); c.nextOpenPos(40, 20, px, py, EFontGlyphType::Grayscale, sheet); - UNSCOPED_INFO("two sheets pending before reset"); + UNSCOPED_INFO("two glyphs on one sheet before reset"); CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); c.reset(); - UNSCOPED_INFO("no Grayscale sheets after reset"); CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 0u); - UNSCOPED_INFO("no Color sheets after reset"); CHECK(c.getNumBitmaps(EFontGlyphType::Color) == 0u); // Re-init and verify pens really did reset back to (4,4). c.init(2, 2); S32 px2 = -1, py2 = -1; U32 sheet2 = 99; c.nextOpenPos(8, 8, px2, py2, EFontGlyphType::Grayscale, sheet2); - UNSCOPED_INFO("post-reset first posX"); CHECK(px2 == 4); - UNSCOPED_INFO("post-reset first posY"); CHECK(py2 == 4); } @@ -364,28 +330,21 @@ namespace S32 px = 0, py = 0; U32 s0 = 0, s1 = 99, s2 = 99; c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, s0); - UNSCOPED_INFO("first alloc lands on sheet 0"); CHECK((S32)s0 == 0); const S32 gen0 = c.getCacheGeneration(); c.releaseSheet(EFontGlyphType::Grayscale, s0); c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, s1); - UNSCOPED_INFO("second alloc recycles slot 0"); CHECK((S32)s1 == 0); const S32 gen1 = c.getCacheGeneration(); - UNSCOPED_INFO("generation advanced across release+recycle"); CHECK(gen1 > gen0); c.releaseSheet(EFontGlyphType::Grayscale, s1); c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, s2); - UNSCOPED_INFO("third alloc recycles slot 0 again"); CHECK((S32)s2 == 0); - UNSCOPED_INFO("generation advanced again"); CHECK(c.getCacheGeneration() > gen1); - UNSCOPED_INFO("getNumBitmaps == 1 (slots recycled, not reserved)"); CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); - UNSCOPED_INFO("slot 0 live after the final alloc"); CHECK_FALSE(c.isSheetReleased(EFontGlyphType::Grayscale, 0)); } @@ -404,7 +363,6 @@ namespace c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, s0); c.releaseSheet(EFontGlyphType::Grayscale, s0); c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, s1); - UNSCOPED_INFO("after release+alloc, recycled onto slot 0"); CHECK((S32)s1 == 0); // Further allocs must keep landing on the recycled sheet (it has @@ -413,12 +371,10 @@ namespace { U32 si = 99; c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, si); - UNSCOPED_INFO("subsequent alloc stays on the recycled sheet"); + UNSCOPED_INFO("a later allocation on the recycled sheet"); REQUIRE((S32)si == 0); } - UNSCOPED_INFO("still 1 sheet slot (no extra allocation)"); CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); - UNSCOPED_INFO("recycled sheet stays live"); CHECK_FALSE(c.isSheetReleased(EFontGlyphType::Grayscale, 0)); } @@ -436,22 +392,22 @@ namespace const S32 g0 = LLFontBitmapCache::getGlobalGeneration(); c1.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, sheet); const S32 g1 = LLFontBitmapCache::getGlobalGeneration(); - UNSCOPED_INFO("c1 alloc bumped global gen"); + UNSCOPED_INFO("after c1 allocated"); CHECK(g1 > g0); c2.nextOpenPos(8, 8, px, py, EFontGlyphType::Color, sheet); const S32 g2 = LLFontBitmapCache::getGlobalGeneration(); - UNSCOPED_INFO("c2 alloc bumped global gen"); + UNSCOPED_INFO("after c2 allocated"); CHECK(g2 > g1); c1.releaseSheet(EFontGlyphType::Grayscale, 0); const S32 g3 = LLFontBitmapCache::getGlobalGeneration(); - UNSCOPED_INFO("c1 release bumped global gen"); + UNSCOPED_INFO("after c1 released"); CHECK(g3 > g2); c2.releaseSheet(EFontGlyphType::Color, 0); const S32 g4 = LLFontBitmapCache::getGlobalGeneration(); - UNSCOPED_INFO("c2 release bumped global gen"); + UNSCOPED_INFO("after c2 released"); CHECK(g4 > g3); } @@ -469,22 +425,17 @@ namespace c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, gs_sheet); c.nextOpenPos(8, 8, px, py, EFontGlyphType::Color, col_sheet); - UNSCOPED_INFO("Grayscale on its own sheet 0"); CHECK((S32)gs_sheet == 0); - UNSCOPED_INFO("Color on its own sheet 0"); CHECK((S32)col_sheet == 0); c.releaseSheet(EFontGlyphType::Color, col_sheet); - UNSCOPED_INFO("Color sheet 0 released"); CHECK(c.isSheetReleased(EFontGlyphType::Color, 0)); - UNSCOPED_INFO("Grayscale sheet 0 unaffected by Color release"); CHECK_FALSE(c.isSheetReleased(EFontGlyphType::Grayscale, 0)); // Allocate again on Grayscale: still lands on sheet 0 (its // trailing slot is live, unaffected by Color side). S32 px2 = 0, py2 = 0; U32 gs_sheet2 = 99; c.nextOpenPos(8, 8, px2, py2, EFontGlyphType::Grayscale, gs_sheet2); - UNSCOPED_INFO("Grayscale alloc stays on sheet 0 (Color release didn't touch it)"); CHECK((S32)gs_sheet2 == 0); } } diff --git a/indra/llrender/tests/llfontbitmapcache_test.cpp b/indra/llrender/tests/llfontbitmapcache_test.cpp index 6a5ded2b13..796e66b2b1 100644 --- a/indra/llrender/tests/llfontbitmapcache_test.cpp +++ b/indra/llrender/tests/llfontbitmapcache_test.cpp @@ -45,11 +45,8 @@ namespace { LLFontBitmapCache a; LLFontBitmapCache b; - UNSCOPED_INFO("two instances get distinct generations"); CHECK(a.getCacheGeneration() != b.getCacheGeneration()); - UNSCOPED_INFO("first generation > 0"); CHECK(a.getCacheGeneration() > 0); - UNSCOPED_INFO("second generation > first"); CHECK(b.getCacheGeneration() > a.getCacheGeneration()); } @@ -61,9 +58,7 @@ namespace const S32 before = LLFontBitmapCache::getGlobalGeneration(); LLFontBitmapCache c; const S32 after = LLFontBitmapCache::getGlobalGeneration(); - UNSCOPED_INFO("global generation strictly increased on construction"); CHECK(after > before); - UNSCOPED_INFO("global generation matches new instance's gen"); CHECK(after == c.getCacheGeneration()); } @@ -76,17 +71,12 @@ namespace c.init(/*max_char_width=*/2, /*max_char_height=*/2); // 2*40=80 → next pow2 ≥ 80 → 128, capped at 1024. - UNSCOPED_INFO("bitmap width set by init"); CHECK(c.getBitmapWidth() == 128); - UNSCOPED_INFO("bitmap height set by init"); CHECK(c.getBitmapHeight() == 128); c.reset(); - UNSCOPED_INFO("bitmap width cleared by reset"); CHECK(c.getBitmapWidth() == 0); - UNSCOPED_INFO("bitmap height cleared by reset"); CHECK(c.getBitmapHeight() == 0); - UNSCOPED_INFO("reset bumped generation"); CHECK(c.getCacheGeneration() > gen_init0); } @@ -96,17 +86,13 @@ namespace TEST_CASE("LLFontBitmapCache: queries out of range or on an empty cache return safe defaults", "[LLFontBitmapCache]") { LLFontBitmapCache c; - UNSCOPED_INFO("getNumBitmaps(Count) returns 0"); CHECK(c.getNumBitmaps(EFontGlyphType::Count) == 0u); - UNSCOPED_INFO("getImageRaw(Count, 0) returns null"); - REQUIRE(c.getImageRaw(EFontGlyphType::Count, 0) == nullptr); - UNSCOPED_INFO("getImageGL(Count, 0) returns null"); - REQUIRE(c.getImageGL(EFontGlyphType::Count, 0) == nullptr); - UNSCOPED_INFO("getImageRaw on empty Grayscale returns null"); - REQUIRE(c.getImageRaw(EFontGlyphType::Grayscale, 0) == nullptr); - UNSCOPED_INFO("getSheetLastUsedTime out-of-range == 0"); + CHECK(c.getImageRaw(EFontGlyphType::Count, 0) == nullptr); + CHECK(c.getImageGL(EFontGlyphType::Count, 0) == nullptr); + UNSCOPED_INFO("an empty Grayscale vector"); + CHECK(c.getImageRaw(EFontGlyphType::Grayscale, 0) == nullptr); CHECK(c.getSheetLastUsedTime(EFontGlyphType::Grayscale, 0) == 0.0); - UNSCOPED_INFO("isSheetReleased on empty cache reports false (slot doesn't exist)"); + UNSCOPED_INFO("a slot that does not exist is not released"); CHECK_FALSE(c.isSheetReleased(EFontGlyphType::Grayscale, 0)); } } diff --git a/indra/llrender/tests/llfontfreetype_gl_test.cpp b/indra/llrender/tests/llfontfreetype_gl_test.cpp index 2292e7fde4..d6cfddfce0 100644 --- a/indra/llrender/tests/llfontfreetype_gl_test.cpp +++ b/indra/llrender/tests/llfontfreetype_gl_test.cpp @@ -64,21 +64,15 @@ namespace if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFtHead(path); - UNSCOPED_INFO("DejaVuSans loaded as head"); REQUIRE(ft.notNull()); LLFontGlyphInfo* gi = ft->getGlyphInfo(L'A', EFontGlyphType::Grayscale); - UNSCOPED_INFO("getGlyphInfo returned an entry"); REQUIRE(gi != nullptr); - UNSCOPED_INFO("glyph has positive width"); CHECK(gi->mWidth > 0); - UNSCOPED_INFO("glyph has positive advance"); CHECK(gi->mXAdvance > 0.f); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - UNSCOPED_INFO("bitmap cache present"); REQUIRE(cache != nullptr); - UNSCOPED_INFO("cache has at least one Grayscale sheet"); CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); } @@ -95,7 +89,6 @@ namespace if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFtHead(path); - UNSCOPED_INFO("DejaVuSans loaded as head"); REQUIRE(ft.notNull()); // Force at least one glyph onto the atlas so collectGarbage // has something to consider. @@ -105,7 +98,7 @@ namespace // doing real work. Both must complete without crashing. ft->collectGarbage(); ft->collectGarbage(); - UNSCOPED_INFO("collectGarbage throttled call returned cleanly"); + UNSCOPED_INFO("both calls returned"); CHECK(true); } @@ -125,15 +118,11 @@ namespace // loadFtHead's parameters are makeKey's, so the manager hands the // head and this lookup the same face. LLPointer ft = loadFtHead(path); - UNSCOPED_INFO("DejaVuSans loaded as head"); REQUIRE(ft.notNull()); (void)ft->getGlyphInfo(L'A', EFontGlyphType::Grayscale); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - { - INFO("face loaded"); - REQUIRE(face.notNull()); - CHECK(face->isValid()); - } + REQUIRE(face.notNull()); + CHECK(face->isValid()); const U32 glyph_a = face->getCharGlyphIndex(L'A'); UNSCOPED_INFO("the head rasterized into this face"); REQUIRE(face->findGlyphInfo(glyph_a, EFontGlyphType::Grayscale) != nullptr); @@ -141,22 +130,19 @@ namespace // Held by the head and by us: survives a sweep, glyph and all. gFontManagerp->collectGarbage(); UNSCOPED_INFO("a held face keeps its glyphs across GC"); - REQUIRE(face->findGlyphInfo(glyph_a, EFontGlyphType::Grayscale) != nullptr); + CHECK(face->findGlyphInfo(glyph_a, EFontGlyphType::Grayscale) != nullptr); UNSCOPED_INFO("a held face is still the cached one"); - REQUIRE(gFontManagerp->getOrCreateFace(makeKey(path)).get() == face.get()); + CHECK(gFontManagerp->getOrCreateFace(makeKey(path)).get() == face.get()); // Held by the cache alone: swept, so the reload is a fresh face. ft = nullptr; face = nullptr; gFontManagerp->collectGarbage(); LLPointer reloaded = gFontManagerp->getOrCreateFace(makeKey(path)); - { - INFO("face reloaded"); - REQUIRE(reloaded.notNull()); - CHECK(reloaded->isValid()); - } + REQUIRE(reloaded.notNull()); + CHECK(reloaded->isValid()); UNSCOPED_INFO("the reloaded face has no glyphs: the cached one was evicted"); - REQUIRE(reloaded->findGlyphInfo(glyph_a, EFontGlyphType::Grayscale) == nullptr); + CHECK(reloaded->findGlyphInfo(glyph_a, EFontGlyphType::Grayscale) == nullptr); } // After two getGlyphInfo calls in a row, both glyphs end up in @@ -172,22 +158,17 @@ namespace if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFtHead(path); - UNSCOPED_INFO("DejaVuSans loaded as head"); REQUIRE(ft.notNull()); LLFontGlyphInfo* g1 = ft->getGlyphInfo(L'A', EFontGlyphType::Grayscale); LLFontGlyphInfo* g2 = ft->getGlyphInfo(L'B', EFontGlyphType::Grayscale); - UNSCOPED_INFO("first glyph allocated"); REQUIRE(g1 != nullptr); - UNSCOPED_INFO("second glyph allocated"); REQUIRE(g2 != nullptr); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - UNSCOPED_INFO("bitmap cache present"); REQUIRE(cache != nullptr); // Both glyphs should land in sheet 0 (a single 1024² sheet // can hold thousands of small ASCII glyphs). - UNSCOPED_INFO("two ASCII glyphs share sheet 0"); CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) == 1u); } @@ -210,27 +191,22 @@ namespace ALFontVarAxes va_light; va_light.wght = 300.f; va_light.wght_set = true; LLPointer ft_light = new LLFontFreetype; - UNSCOPED_INFO("weight=300 load"); CHECK(ft_light->loadFace(path, 32.f, 96.f, 96.f, /*is_fallback=*/false, 0, EFontHinting::DEFAULT, /*flags=*/0, va_light)); ALFontVarAxes va_black; va_black.wght = 900.f; va_black.wght_set = true; LLPointer ft_black = new LLFontFreetype; - UNSCOPED_INFO("weight=900 load"); CHECK(ft_black->loadFace(path, 32.f, 96.f, 96.f, /*is_fallback=*/false, 0, EFontHinting::DEFAULT, /*flags=*/0, va_black)); const F32 adv_light = ft_light->getXAdvance(L'A'); const F32 adv_black = ft_black->getXAdvance(L'A'); - UNSCOPED_INFO("light advance positive"); CHECK(adv_light > 0.f); - UNSCOPED_INFO("black advance positive"); CHECK(adv_black > 0.f); // Heavier weight produces wider 'A'. The diff at 32pt 96dpi // between weight=300 and weight=900 is ~1.5-2 px for Inter. - UNSCOPED_INFO("weight=900 'A' advance > weight=300 'A' advance"); CHECK(adv_black > adv_light + 0.5f); } @@ -246,18 +222,15 @@ namespace if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFtHead(path); - UNSCOPED_INFO("DejaVuSans loaded as head"); REQUIRE(ft.notNull()); LLFontGlyphInfo* g1 = ft->getGlyphInfo(L'A', EFontGlyphType::Grayscale); - UNSCOPED_INFO("first glyph allocated"); REQUIRE(g1 != nullptr); // Use getBitmapCache() (non-const) for releaseSheet; the const // overload exposed via getFontBitmapCache() is for queries only. LLFontBitmapCache* cache = ft->getBitmapCache(); - UNSCOPED_INFO("bitmap cache present"); REQUIRE(cache != nullptr); - UNSCOPED_INFO("started with one Grayscale sheet"); + UNSCOPED_INFO("before the release"); CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) == 1u); const S32 gen_before = cache->getCacheGeneration(); @@ -278,19 +251,15 @@ namespace return false; }); cache->releaseSheet(EFontGlyphType::Grayscale, 0); - UNSCOPED_INFO("sheet 0 reports released"); CHECK(cache->isSheetReleased(EFontGlyphType::Grayscale, 0)); // New glyph rebuilds into the recycled slot. Picking a glyph not // already cached avoids hitting the cmap-cache branch. LLFontGlyphInfo* g2 = ft->getGlyphInfo(L'B', EFontGlyphType::Grayscale); - UNSCOPED_INFO("second glyph allocated post-release"); REQUIRE(g2 != nullptr); - UNSCOPED_INFO("released slot recycled (no sheet-vector growth)"); + UNSCOPED_INFO("the released slot was recycled"); CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) == 1u); - UNSCOPED_INFO("recycled slot is live again"); CHECK_FALSE(cache->isSheetReleased(EFontGlyphType::Grayscale, 0)); - UNSCOPED_INFO("cache generation advanced after release+alloc"); CHECK(cache->getCacheGeneration() > gen_before); } @@ -309,20 +278,17 @@ namespace LLPointer head_a = loadFtHead(a_path); LLPointer head_b = loadFtHead(b_path); - { - INFO("both heads loaded"); - REQUIRE(head_a.notNull()); - REQUIRE(head_b.notNull()); - } + REQUIRE(head_a.notNull()); + REQUIRE(head_b.notNull()); const S32 gen0 = LLFontBitmapCache::getGlobalGeneration(); (void)head_a->getGlyphInfo(L'A', EFontGlyphType::Grayscale); const S32 gen1 = LLFontBitmapCache::getGlobalGeneration(); - UNSCOPED_INFO("gen advanced after head_a raster"); + UNSCOPED_INFO("after head_a rasterized"); CHECK(gen1 > gen0); (void)head_b->getGlyphInfo(L'B', EFontGlyphType::Grayscale); const S32 gen2 = LLFontBitmapCache::getGlobalGeneration(); - UNSCOPED_INFO("gen advanced after head_b raster"); + UNSCOPED_INFO("after head_b rasterized"); CHECK(gen2 > gen1); } @@ -345,29 +311,22 @@ namespace LLPointer head_a = loadFtHead(path); LLPointer head_b = loadFtHead(path); - { - INFO("both heads loaded"); - REQUIRE(head_a.notNull()); - REQUIRE(head_b.notNull()); - } + REQUIRE(head_a.notNull()); + REQUIRE(head_b.notNull()); // Same ALFontFaceKey -> shared ALFontFace via getOrCreateFace. - UNSCOPED_INFO("siblings share the underlying face wrapper"); REQUIRE(head_a->getFontFace() == head_b->getFontFace()); // Cache a glyph through both heads. Face dedup means both // calls return the same entry pointer. LLFontGlyphInfo* a_gi = head_a->getGlyphInfo(L'A', EFontGlyphType::Grayscale); LLFontGlyphInfo* b_gi = head_b->getGlyphInfo(L'A', EFontGlyphType::Grayscale); - UNSCOPED_INFO("head_a got glyph"); REQUIRE(a_gi != nullptr); - UNSCOPED_INFO("head_b got glyph"); REQUIRE(b_gi != nullptr); - UNSCOPED_INFO("shared face returns the same glyph entry"); REQUIRE(a_gi == b_gi); const auto pre_entry = a_gi->mPhaseSlots[0].mBitmapEntry; { - INFO("phase 0 bitmap entry references a real sheet"); + INFO("phase 0 references a real sheet"); CHECK(pre_entry.first == EFontGlyphType::Grayscale); CHECK(pre_entry.second >= 0); } @@ -379,7 +338,6 @@ namespace // pointer; post-fix there is no per-head cache to leak. const auto target = pre_entry; LLFontBitmapCache* cache = head_a->getBitmapCache(); - UNSCOPED_INFO("bitmap cache present"); REQUIRE(cache != nullptr); head_a->getFontFace()->erase_glyph_entries( [target](const LLFontGlyphInfo* gi) @@ -393,7 +351,6 @@ namespace return false; }); cache->releaseSheet(target.first, static_cast(target.second)); - UNSCOPED_INFO("target sheet reports released"); CHECK(cache->isSheetReleased(target.first, static_cast(target.second))); // head_b looks up the same glyph again. The lookup must: @@ -401,10 +358,8 @@ namespace // 2. produce a non-null entry, // 3. land on a live sheet (not the just-released one). LLFontGlyphInfo* b_gi2 = head_b->getGlyphInfo(L'A', EFontGlyphType::Grayscale); - UNSCOPED_INFO("head_b got fresh glyph after sibling-driven eviction"); REQUIRE(b_gi2 != nullptr); const auto post_entry = b_gi2->mPhaseSlots[0].mBitmapEntry; - UNSCOPED_INFO("post-eviction phase 0 references a live sheet"); CHECK_FALSE(cache->isSheetReleased(post_entry.first, static_cast(post_entry.second))); } @@ -422,15 +377,13 @@ namespace SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFtHead(path); - UNSCOPED_INFO("ft loaded"); REQUIRE(ft.notNull()); LLFontGlyphInfo* g1 = ft->getGlyphInfo(L'Z', EFontGlyphType::Grayscale); - UNSCOPED_INFO("got glyph"); REQUIRE(g1 != nullptr); const auto target = g1->mPhaseSlots[0].mBitmapEntry; { - INFO("bitmap entry valid"); + INFO("phase 0 references a real sheet"); CHECK(target.first == EFontGlyphType::Grayscale); CHECK(target.second >= 0); } @@ -454,10 +407,8 @@ namespace // (now empty for this glyph), falls through to addShapedGlyphFromFont, // and rasterizes onto a live sheet. LLFontGlyphInfo* g2 = ft->getGlyphInfo(L'Z', EFontGlyphType::Grayscale); - UNSCOPED_INFO("re-requested glyph allocated post-eviction"); REQUIRE(g2 != nullptr); const auto post = g2->mPhaseSlots[0].mBitmapEntry; - UNSCOPED_INFO("post-eviction bitmap entry references a live sheet"); CHECK_FALSE(ft->getBitmapCache()->isSheetReleased( post.first, static_cast(post.second))); } diff --git a/indra/llrender/tests/llfontfreetype_test.cpp b/indra/llrender/tests/llfontfreetype_test.cpp index 63d9ae8bbe..d5efb10982 100644 --- a/indra/llrender/tests/llfontfreetype_test.cpp +++ b/indra/llrender/tests/llfontfreetype_test.cpp @@ -74,29 +74,26 @@ namespace { ALFontFaceKey a{ "x.ttf", 0, 14.f, 96.f, 96.f, EFontHinting::DEFAULT, 0 }; ALFontFaceKey b = a; - UNSCOPED_INFO("identical keys equal"); CHECK(a == b); - UNSCOPED_INFO("identical keys hash equal"); CHECK(hash_value(a) == hash_value(b)); b.point_size = 18.f; - UNSCOPED_INFO("differing point_size keys not equal"); + UNSCOPED_INFO("point_size differs"); CHECK_FALSE(a == b); b = a; b.face_index = 1; - UNSCOPED_INFO("differing face_index keys not equal"); + UNSCOPED_INFO("face_index differs"); CHECK_FALSE(a == b); b = a; b.var_axes.wght = 600.f; b.var_axes.wght_set = true; - UNSCOPED_INFO("differing wght keys not equal"); + UNSCOPED_INFO("wght differs"); CHECK_FALSE(a == b); b = a; b.hinting = EFontHinting::FORCE_AUTOHINT; - UNSCOPED_INFO("differing hinting keys not equal"); + UNSCOPED_INFO("hinting differs"); CHECK_FALSE(a == b); b = a; b.flags = LLFontGL::BOLD; - UNSCOPED_INFO("differing flags keys not equal"); + UNSCOPED_INFO("flags differ"); CHECK_FALSE(a == b); } - // hasColrV1: Noto-COLRv1 yes, DejaVuSans no. TEST_CASE_METHOD(alfontface_data, "ALFontFace: hasColrV1 is true for Noto-COLRv1 and false for DejaVuSans", "[ALFontFace]") { const std::string noto = std::string(kFontDir) + "Noto-COLRv1.ttf"; @@ -106,14 +103,9 @@ namespace LLPointer nf = gFontManagerp->getOrCreateFace(makeKey(noto)); LLPointer df = gFontManagerp->getOrCreateFace(makeKey(dv)); - { - INFO("both faces valid"); - CHECK(nf->isValid()); - CHECK(df->isValid()); - } - UNSCOPED_INFO("Noto-COLRv1 hasColrV1"); + CHECK(nf->isValid()); + CHECK(df->isValid()); CHECK(nf->hasColrV1()); - UNSCOPED_INFO("DejaVuSans !hasColrV1"); CHECK_FALSE(df->hasColrV1()); } @@ -128,9 +120,7 @@ namespace LLPointer nf = gFontManagerp->getOrCreateFace(makeKey(noto)); LLPointer df = gFontManagerp->getOrCreateFace(makeKey(dv)); - UNSCOPED_INFO("Noto-COLRv1 hasColor"); CHECK(nf->hasColor()); - UNSCOPED_INFO("DejaVuSans !hasColor"); CHECK_FALSE(df->hasColor()); } @@ -146,9 +136,7 @@ namespace LLPointer nf = gFontManagerp->getOrCreateFace(makeKey(noto)); LLPointer df = gFontManagerp->getOrCreateFace(makeKey(dv)); - UNSCOPED_INFO("Noto-COLRv1 !hasSvg"); CHECK_FALSE(nf->hasSvg()); - UNSCOPED_INFO("DejaVuSans !hasSvg"); CHECK_FALSE(df->hasSvg()); } @@ -168,14 +156,9 @@ namespace LLPointer def = gFontManagerp->getOrCreateFace(k_default); LLPointer bld = gFontManagerp->getOrCreateFace(k_bold); - { - INFO("both loaded"); - CHECK(def->isValid()); - CHECK(bld->isValid()); - } - UNSCOPED_INFO("wght unset -> wghtAxisSet=false"); + CHECK(def->isValid()); + CHECK(bld->isValid()); CHECK_FALSE(def->wghtAxisSet()); - UNSCOPED_INFO("wght=600 on variable face -> wghtAxisSet=true"); CHECK(bld->wghtAxisSet()); } @@ -190,9 +173,7 @@ namespace SKIP("DejaVuSans + DejaVuSansMono required"); LLPointer m = gFontManagerp->getOrCreateFace(makeKey(mono)); LLPointer p = gFontManagerp->getOrCreateFace(makeKey(prop)); - UNSCOPED_INFO("Mono isFixedWidth"); CHECK(m->isFixedWidth()); - UNSCOPED_INFO("Prop !isFixedWidth"); CHECK_FALSE(p->isFixedWidth()); } @@ -208,14 +189,13 @@ namespace const U32 g_a = f->getCharGlyphIndex(L'A'); const U32 g_a_again = f->getCharGlyphIndex(L'A'); - UNSCOPED_INFO("'A' has a glyph"); CHECK(g_a != 0u); - UNSCOPED_INFO("repeat 'A' lookup hits cache (same value)"); + UNSCOPED_INFO("a repeat lookup hits the cache"); CHECK(g_a == g_a_again); - UNSCOPED_INFO("absent codepoint -> 0"); + UNSCOPED_INFO("an absent codepoint"); CHECK(f->getCharGlyphIndex(0xE000) == 0u); // Cache the miss too — second call should also be 0. - UNSCOPED_INFO("absent codepoint cached miss is also 0"); + UNSCOPED_INFO("the cached miss"); CHECK(f->getCharGlyphIndex(0xE000) == 0u); } @@ -231,9 +211,8 @@ namespace LLPointer a = gFontManagerp->getOrCreateFace(makeKey(path)); LLPointer b = gFontManagerp->getOrCreateFace(makeKey(path)); - UNSCOPED_INFO("a == b (shared face)"); REQUIRE(a.get() == b.get()); - UNSCOPED_INFO("refcount >= 3 (cache + a + b)"); + UNSCOPED_INFO("refs held by the cache and a and b"); CHECK(a->getNumRefs() >= 3); ALFontFace* raw = a.get(); @@ -246,7 +225,7 @@ namespace // pointer (cache hit), not a freshly-loaded face. LLPointer c = gFontManagerp->getOrCreateFace(makeKey(path)); UNSCOPED_INFO("cache kept face alive after both refs dropped"); - REQUIRE(c.get() == raw); + CHECK(c.get() == raw); } // resetBitmapCache clears atlas state without destroying the face. @@ -257,15 +236,12 @@ namespace if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer f = gFontManagerp->getOrCreateFace(makeKey(path)); - UNSCOPED_INFO("face valid before reset"); CHECK(f->isValid()); f->resetBitmapCache(); - UNSCOPED_INFO("face still valid after resetBitmapCache"); CHECK(f->isValid()); UNSCOPED_INFO("cmap survives reset"); CHECK(f->getCharGlyphIndex(L'A') != 0u); - UNSCOPED_INFO("bitmap cache pointer still present"); - REQUIRE(f->getBitmapCache() != nullptr); + CHECK(f->getBitmapCache() != nullptr); } // paletteIndex() is computed at load time from LLFontGL::sUseDarkEmojiPalette @@ -295,7 +271,7 @@ namespace CHECK(f->isValid()); } pi_default = f->paletteIndex(); - UNSCOPED_INFO("paletteIndex defaults to 0 with dark=false"); + UNSCOPED_INFO("loaded with dark=false"); CHECK(pi_default == 0u); } // Drop our ref; collectGarbage evicts since cache holds the only ref. @@ -323,7 +299,6 @@ namespace if (pi_dark == 0) SKIP("font has no FT_PALETTE_FOR_DARK_BACKGROUND palette"); - UNSCOPED_INFO("dark=true selected a non-zero palette index"); CHECK(pi_dark != 0u); } @@ -341,12 +316,8 @@ namespace k.var_axes.wght = 600.f; k.var_axes.wght_set = true; LLPointer f = gFontManagerp->getOrCreateFace(k); - { - INFO("face loaded"); - REQUIRE(f.notNull()); - CHECK(f->isValid()); - } - UNSCOPED_INFO("non-variable face leaves wghtAxisSet=false"); + REQUIRE(f.notNull()); + CHECK(f->isValid()); CHECK_FALSE(f->wghtAxisSet()); } @@ -367,20 +338,14 @@ namespace k_600.var_axes.wght = 600.f; k_600.var_axes.wght_set = true; - UNSCOPED_INFO("k_400 != k_600"); CHECK_FALSE(k_400 == k_600); - UNSCOPED_INFO("hash differs by weight"); CHECK(hash_value(k_400) != hash_value(k_600)); LLPointer f400 = gFontManagerp->getOrCreateFace(k_400); LLPointer f600 = gFontManagerp->getOrCreateFace(k_600); - { - INFO("both faces valid"); - CHECK(f400->isValid()); - CHECK(f600->isValid()); - } - UNSCOPED_INFO("distinct weights produce distinct face entries"); - REQUIRE(f400.get() != f600.get()); + CHECK(f400->isValid()); + CHECK(f600->isValid()); + CHECK(f400.get() != f600.get()); } // ital/wdth/slnt axis plumbing: distinct ALFontVarAxes values must @@ -406,42 +371,29 @@ namespace k_slnt.var_axes.slnt = -12.f; k_slnt.var_axes.slnt_set = true; - UNSCOPED_INFO("default != ital"); CHECK_FALSE(k_default == k_ital); - UNSCOPED_INFO("default != wdth"); CHECK_FALSE(k_default == k_wdth); - UNSCOPED_INFO("default != slnt"); CHECK_FALSE(k_default == k_slnt); - UNSCOPED_INFO("ital != wdth"); CHECK_FALSE(k_ital == k_wdth); - UNSCOPED_INFO("wdth != slnt"); CHECK_FALSE(k_wdth == k_slnt); - UNSCOPED_INFO("default hash differs from ital"); CHECK(hash_value(k_default) != hash_value(k_ital)); - UNSCOPED_INFO("default hash differs from wdth"); CHECK(hash_value(k_default) != hash_value(k_wdth)); - UNSCOPED_INFO("default hash differs from slnt"); CHECK(hash_value(k_default) != hash_value(k_slnt)); // Identical axis values produce equal keys + equal hashes. ALFontFaceKey k_ital_dup = makeKey(path); k_ital_dup.var_axes.ital = 1.f; k_ital_dup.var_axes.ital_set = true; - UNSCOPED_INFO("ital == ital_dup"); CHECK(k_ital == k_ital_dup); - UNSCOPED_INFO("ital hash matches dup"); CHECK(hash_value(k_ital) == hash_value(k_ital_dup)); // Cache identity round-trips through the face manager. LLPointer fd = gFontManagerp->getOrCreateFace(k_default); LLPointer fi = gFontManagerp->getOrCreateFace(k_ital); - { - INFO("both faces valid"); - CHECK(fd->isValid()); - CHECK(fi->isValid()); - } - UNSCOPED_INFO("distinct ital settings produce distinct face entries"); - REQUIRE(fd.get() != fi.get()); + CHECK(fd->isValid()); + CHECK(fi->isValid()); + UNSCOPED_INFO("distinct ital settings are distinct cached faces"); + CHECK(fd.get() != fi.get()); } // Faces that don't expose the requested axis leave the matching @@ -464,16 +416,10 @@ namespace k.var_axes.slnt_set = true; LLPointer f = gFontManagerp->getOrCreateFace(k); - { - INFO("face loaded"); - REQUIRE(f.notNull()); - CHECK(f->isValid()); - } - UNSCOPED_INFO("DejaVu lacks ital axis -> italAxisSet false"); + REQUIRE(f.notNull()); + CHECK(f->isValid()); CHECK_FALSE(f->italAxisSet()); - UNSCOPED_INFO("DejaVu lacks wdth axis -> wdthAxisSet false"); CHECK_FALSE(f->wdthAxisSet()); - UNSCOPED_INFO("DejaVu lacks slnt axis -> slntAxisSet false"); CHECK_FALSE(f->slntAxisSet()); } @@ -485,35 +431,31 @@ namespace { ALFontVarAxes a; ALFontVarAxes b; - UNSCOPED_INFO("default ctor equal"); CHECK(a == b); // Differing values with *_set=false still equal. b.ital = 0.5f; b.wdth = 99.f; b.slnt = -3.f; - UNSCOPED_INFO("unset values are ignored in equality"); + UNSCOPED_INFO("unset values are ignored"); CHECK(a == b); // Once a set flag flips, value differences matter. a.ital_set = true; a.ital = 1.f; b.ital_set = true; b.ital = 0.5f; - UNSCOPED_INFO("set ital with different values is not equal"); + UNSCOPED_INFO("ital set on both with different values"); CHECK_FALSE(a == b); b.ital = 1.f; - UNSCOPED_INFO("set ital with matching values is equal"); + UNSCOPED_INFO("ital set on both with the same value"); CHECK(a == b); } - // initClass is idempotent — repeat calls don't replace gFontManagerp. TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: initClass is idempotent and keeps the same gFontManagerp", "[LLFontFreetype]") { - UNSCOPED_INFO("gFontManagerp non-null after init"); REQUIRE(gFontManagerp != nullptr); LLFontManager* before = gFontManagerp; LLFontManager::initClass(); // second call: no-op - UNSCOPED_INFO("gFontManagerp unchanged on repeat initClass"); - REQUIRE(gFontManagerp == before); + CHECK(gFontManagerp == before); } // getOrCreateFace returns the same face for equal keys; differing @@ -528,37 +470,23 @@ namespace LLPointer a = gFontManagerp->getOrCreateFace(makeKey(path, 14.f)); LLPointer b = gFontManagerp->getOrCreateFace(makeKey(path, 14.f)); LLPointer c = gFontManagerp->getOrCreateFace(makeKey(path, 18.f)); - { - INFO("face a loaded"); - REQUIRE(a.notNull()); - CHECK(a->isValid()); - } - { - INFO("face b loaded"); - REQUIRE(b.notNull()); - CHECK(b->isValid()); - } - { - INFO("face c loaded"); - REQUIRE(c.notNull()); - CHECK(c->isValid()); - } - UNSCOPED_INFO("equal keys -> same face pointer"); - REQUIRE(a.get() == b.get()); - UNSCOPED_INFO("different point_size -> distinct face"); - REQUIRE(a.get() != c.get()); + REQUIRE(a.notNull()); + CHECK(a->isValid()); + REQUIRE(b.notNull()); + CHECK(b->isValid()); + REQUIRE(c.notNull()); + CHECK(c->isValid()); + CHECK(a.get() == b.get()); + CHECK(a.get() != c.get()); } - // loadFace on a real file returns true and populates getName. TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: loadFace on a real file succeeds and getName is its path", "[LLFontFreetype]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("loadFace succeeded"); REQUIRE(ft.notNull()); - UNSCOPED_INFO("getName matches filename"); CHECK(ft->getName() == path); } @@ -570,7 +498,6 @@ namespace const bool ok = ft->loadFace("does/not/exist.ttf", 14.f, 96.f, 96.f, true, 0, EFontHinting::DEFAULT, 0); - UNSCOPED_INFO("loadFace returns false on missing file"); CHECK_FALSE(ok); // Don't deref ft beyond this — destructor must run cleanly. ft = nullptr; @@ -587,19 +514,16 @@ namespace if (!fileExists(path)) SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - UNSCOPED_INFO("DejaVuSans loaded"); REQUIRE(ft.notNull()); const U32 a1 = ft->getCharGlyphIndex(L'A'); const U32 a2 = ft->getCharGlyphIndex(L'A'); - UNSCOPED_INFO("'A' has a non-zero glyph index"); CHECK(a1 != 0u); - UNSCOPED_INFO("repeated lookup returns same value (cache hit)"); + UNSCOPED_INFO("a repeat lookup hits the cache"); CHECK(a1 == a2); // U+E000 is in the BMP private-use area — DejaVuSans doesn't // populate it; getCharGlyphIndex must return 0 (cached miss // is meaningful — it lets the renderer fall through to // fallbacks without re-walking the cmap). - UNSCOPED_INFO("PUA codepoint has glyph index 0"); CHECK(ft->getCharGlyphIndex(0xE000) == 0u); } @@ -616,16 +540,11 @@ namespace LLPointer dv = loadFt(dejavu); LLPointer nt = loadFt(noto); - { - INFO("both loaded"); - REQUIRE(dv.notNull()); - REQUIRE(nt.notNull()); - } - UNSCOPED_INFO("DejaVuSans covers 'a'"); + REQUIRE(dv.notNull()); + REQUIRE(nt.notNull()); CHECK(dv->faceHasGlyph(L'a')); - UNSCOPED_INFO("Noto-COLRv1 lacks U+FE0F"); CHECK_FALSE(nt->faceHasGlyph(0xFE0F)); - UNSCOPED_INFO("Noto-COLRv1 covers ZWJ (U+200D)"); + UNSCOPED_INFO("U+200D is ZWJ"); CHECK(nt->faceHasGlyph(0x200D)); } @@ -643,14 +562,11 @@ namespace 14.f, -1, EFontHinting::DEFAULT); LLPointer b = loadFt(path, /*is_fallback=*/true, 14.f, -1, EFontHinting::FORCE_AUTOHINT); - { - INFO("both loaded"); - REQUIRE(a.notNull()); - REQUIRE(b.notNull()); - } - UNSCOPED_INFO("DEFAULT hinting -> useSubpixelPen=false"); + REQUIRE(a.notNull()); + REQUIRE(b.notNull()); + UNSCOPED_INFO("DEFAULT hinting"); CHECK_FALSE(a->useSubpixelPen()); - UNSCOPED_INFO("FORCE_AUTOHINT on non-color font -> useSubpixelPen=true"); + UNSCOPED_INFO("FORCE_AUTOHINT on a font without color"); CHECK(b->useSubpixelPen()); // A color font (Noto-COLRv1) with FORCE_AUTOHINT still reports @@ -660,9 +576,8 @@ namespace { LLPointer c = loadFt(colr, true, 14.f, -1, EFontHinting::FORCE_AUTOHINT); - UNSCOPED_INFO("Noto-COLRv1 loaded"); REQUIRE(c.notNull()); - UNSCOPED_INFO("color font -> useSubpixelPen=false even with FORCE_AUTOHINT"); + UNSCOPED_INFO("FORCE_AUTOHINT on a color font"); CHECK_FALSE(c->useSubpixelPen()); } } @@ -678,17 +593,13 @@ namespace LLPointer m = loadFt(mono); LLPointer p = loadFt(prop); - UNSCOPED_INFO("Mono is fixed-width"); CHECK(m->isFixedWidth()); - UNSCOPED_INFO("Prop is not fixed-width"); CHECK_FALSE(p->isFixedWidth()); - UNSCOPED_INFO("ligatures default off on Mono"); + UNSCOPED_INFO("ligatures default off"); CHECK_FALSE(m->getAllowMonospaceLigatures()); m->setAllowMonospaceLigatures(true); - UNSCOPED_INFO("ligatures-on toggle takes effect"); CHECK(m->getAllowMonospaceLigatures()); m->setAllowMonospaceLigatures(false); - UNSCOPED_INFO("ligatures-off toggle takes effect"); CHECK_FALSE(m->getAllowMonospaceLigatures()); } @@ -703,9 +614,7 @@ namespace LLPointer n = loadFt(normal); LLPointer b = loadFt(bold); - UNSCOPED_INFO("normal style has no BOLD bit"); CHECK((n->getStyle() & LLFontGL::BOLD) == 0); - UNSCOPED_INFO("bold file sets BOLD bit"); CHECK((b->getStyle() & LLFontGL::BOLD) != 0); } @@ -724,32 +633,26 @@ namespace LLPointer head = loadFt(dejavu); LLPointer cjk = loadFt(han); LLPointer emo = loadFt(emoji); - { - INFO("all three loaded"); - REQUIRE(head.notNull()); - REQUIRE(cjk.notNull()); - REQUIRE(emo.notNull()); - } + REQUIRE(head.notNull()); + REQUIRE(cjk.notNull()); + REQUIRE(emo.notNull()); head->addFallbackFont(cjk); head->addFallbackFont(emo); U32 idx = 0; // ASCII 'a' lives in the head's cmap → head wins. - UNSCOPED_INFO("'a' resolves to head"); - REQUIRE(head->selectShapingFace(L'a', idx) == head.get()); - UNSCOPED_INFO("head produced a non-zero glyph for 'a'"); + CHECK(head->selectShapingFace(L'a', idx) == head.get()); + UNSCOPED_INFO("the head's glyph for 'a'"); CHECK(idx != 0u); // U+4F60 你 lives in SourceHanSans, not in DejaVuSans → CJK fallback. idx = 0; - UNSCOPED_INFO("CJK codepoint resolves to SourceHanSans fallback"); - REQUIRE(head->selectShapingFace(0x4F60, idx) == cjk.get()); + CHECK(head->selectShapingFace(0x4F60, idx) == cjk.get()); // U+1F525 (fire emoji) lives in Noto-COLRv1, not in either text // face → emoji fallback wins. idx = 0; - UNSCOPED_INFO("emoji codepoint resolves to Noto-COLRv1 fallback"); - REQUIRE(head->selectShapingFace(0x1F525, idx) == emo.get()); + CHECK(head->selectShapingFace(0x1F525, idx) == emo.get()); } // mShapingFaceResolution caches selectShapingFace decisions; adding @@ -765,36 +668,32 @@ namespace LLPointer head = loadFt(dejavu); LLPointer emo = loadFt(emoji); - { - INFO("both loaded"); - REQUIRE(head.notNull()); - REQUIRE(emo.notNull()); - } + REQUIRE(head.notNull()); + REQUIRE(emo.notNull()); // First lookup, no fallback registered. 'a' resolves to head; // emoji resolves to head with idx=0 (no fallback covers it). U32 idx_a1 = 0, idx_e1 = 0; const LLFontFreetype* a1 = head->selectShapingFace(L'a', idx_a1); const LLFontFreetype* e1 = head->selectShapingFace(0x1F525, idx_e1); - UNSCOPED_INFO("'a' first resolution -> head"); - REQUIRE(a1 == head.get()); - UNSCOPED_INFO("emoji first resolution -> head (no fallback)"); - REQUIRE(e1 == head.get()); - UNSCOPED_INFO("emoji on plain head produced glyph 0"); - CHECK(idx_e1 == 0u); + CHECK(a1 == head.get()); + { + INFO("emoji with no fallback yet"); + CHECK(e1 == head.get()); + CHECK(idx_e1 == 0u); + } // Second lookup hits the cache and returns the same pointer. U32 idx_a2 = 0; - UNSCOPED_INFO("repeat 'a' lookup hits cache"); - REQUIRE(head->selectShapingFace(L'a', idx_a2) == head.get()); + UNSCOPED_INFO("a repeat lookup hits the cache"); + CHECK(head->selectShapingFace(L'a', idx_a2) == head.get()); // Add the emoji fallback. The cache must invalidate so the next // emoji lookup re-runs the walker through the new chain. head->addFallbackFont(emo); U32 idx_e2 = 0; - UNSCOPED_INFO("after addFallbackFont, emoji re-resolves to fallback"); - REQUIRE(head->selectShapingFace(0x1F525, idx_e2) == emo.get()); + CHECK(head->selectShapingFace(0x1F525, idx_e2) == emo.get()); } // Functor gating: a fallback whose char_functor returns false for @@ -809,11 +708,8 @@ namespace LLPointer head = loadFt(dejavu); LLPointer emo = loadFt(emoji); - { - INFO("both loaded"); - REQUIRE(head.notNull()); - REQUIRE(emo.notNull()); - } + REQUIRE(head.notNull()); + REQUIRE(emo.notNull()); // Functor accepts only emoji-range codepoints; rejects ASCII. // If TwemojiSVG happens to carry ASCII outlines, the functor @@ -822,11 +718,11 @@ namespace head->addFallbackFont(emo, pictograph_only); U32 idx = 0; - UNSCOPED_INFO("'a' resolves to head despite Twemoji ASCII coverage"); - REQUIRE(head->selectShapingFace(L'a', idx) == head.get()); + UNSCOPED_INFO("the functor rejects 'a' for Twemoji"); + CHECK(head->selectShapingFace(L'a', idx) == head.get()); // Functor allows emoji range, so emoji codepoints route to Twemoji. idx = 0; - UNSCOPED_INFO("emoji codepoint passes functor and resolves to Twemoji"); - REQUIRE(head->selectShapingFace(0x1F525, idx) == emo.get()); + UNSCOPED_INFO("the functor passes the emoji to Twemoji"); + CHECK(head->selectShapingFace(0x1F525, idx) == emo.get()); } } diff --git a/indra/llrender/tests/llfontgl_test.cpp b/indra/llrender/tests/llfontgl_test.cpp index ab4d0b1074..bbf15b1af1 100644 --- a/indra/llrender/tests/llfontgl_test.cpp +++ b/indra/llrender/tests/llfontgl_test.cpp @@ -111,10 +111,8 @@ namespace /*font_overrides=*/LLSD(), /*create_gl_textures=*/true); - UNSCOPED_INFO("default fonts loaded"); CHECK(LLFontGL::loadDefaultFonts()); - UNSCOPED_INFO("SansSerif resolves"); - REQUIRE(LLFontGL::getFontSansSerif() != nullptr); + CHECK(LLFontGL::getFontSansSerif() != nullptr); } // After ASCII rasterization, the SansSerif font's bitmap cache @@ -137,7 +135,6 @@ namespace LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("SansSerif resolves"); REQUIRE(font != nullptr); // Force the ASCII range to rasterize so the atlas isn't empty. @@ -146,19 +143,14 @@ namespace font->generateASCIIglyphs(); const LLFontFreetype* ft = font->getFontFreetype(); - UNSCOPED_INFO("LLFontFreetype present"); REQUIRE(ft != nullptr); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - UNSCOPED_INFO("bitmap cache present"); REQUIRE(cache != nullptr); - UNSCOPED_INFO("at least one grayscale atlas page"); CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); LLImageGL* page = cache->getImageGL(EFontGlyphType::Grayscale, 0); - UNSCOPED_INFO("page 0 LLImageGL present"); REQUIRE(page != nullptr); - UNSCOPED_INFO("page 0 has a live GL texture name"); CHECK(page->getTexName() != 0); } @@ -173,12 +165,10 @@ namespace LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* mono = LLFontGL::getFontMonospace(); - UNSCOPED_INFO("Monospace resolves"); REQUIRE(mono != nullptr); const LLFontFreetype* ft = mono->getFontFreetype(); - UNSCOPED_INFO("Monospace has a freetype"); REQUIRE(ft != nullptr); - UNSCOPED_INFO("Monospace freetype is fixed-width: " + ft->getName()); + INFO("Monospace resolved to " << ft->getName()); CHECK(ft->isFixedWidth()); } @@ -193,17 +183,12 @@ namespace LLSD(), /*create_gl_textures=*/true); LLFontGL* small = LLFontGL::getFontSansSerifSmall(); LLFontGL* big = LLFontGL::getFontSansSerifBig(); - UNSCOPED_INFO("Small SansSerif resolves"); REQUIRE(small != nullptr); - UNSCOPED_INFO("Big SansSerif resolves"); REQUIRE(big != nullptr); const LLFontFreetype* fs = small->getFontFreetype(); const LLFontFreetype* fb = big->getFontFreetype(); - UNSCOPED_INFO("Small line-height > 0"); CHECK(fs->getLineHeight() > 0.f); - UNSCOPED_INFO("Big line-height > 0"); CHECK(fb->getLineHeight() > 0.f); - UNSCOPED_INFO("Big line-height > Small line-height"); CHECK(fb->getLineHeight() > fs->getLineHeight()); } @@ -219,10 +204,8 @@ namespace LLSD(), /*create_gl_textures=*/true); LLFontGL* a = LLFontGL::getFontSansSerif(); LLFontGL* b = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("first resolve non-null"); REQUIRE(a != nullptr); - UNSCOPED_INFO("second resolve returns the same instance"); - REQUIRE(a == b); + CHECK(a == b); } // generateASCIIglyphs is idempotent — repeat calls don't allocate @@ -235,19 +218,16 @@ namespace LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); font->generateASCIIglyphs(); const LLFontFreetype* ft = font->getFontFreetype(); const LLFontBitmapCache* bc = ft->getFontBitmapCache(); const U32 sheets_after_first = bc->getNumBitmaps(EFontGlyphType::Grayscale); - UNSCOPED_INFO("at least one sheet after first generate"); CHECK(sheets_after_first >= 1u); font->generateASCIIglyphs(); const U32 sheets_after_second = bc->getNumBitmaps(EFontGlyphType::Grayscale); - UNSCOPED_INFO("second generate is idempotent (no extra sheets)"); CHECK(sheets_after_second == sheets_after_first); } @@ -261,10 +241,8 @@ namespace LLSD(), /*create_gl_textures=*/true); LLFontGL* a = LLFontGL::getFontByName("SANSSERIF"); LLFontGL* b = LLFontGL::getFontByName("SANSSERIF"); - UNSCOPED_INFO("legacy name resolves to non-null"); REQUIRE(a != nullptr); - UNSCOPED_INFO("repeat lookup returns same instance"); - REQUIRE(a == b); + CHECK(a == b); } // collectGarbage on the font face does not crash and (since @@ -279,10 +257,8 @@ namespace LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); const LLFontFreetype* ft = font->getFontFreetype(); - UNSCOPED_INFO("freetype present"); REQUIRE(ft != nullptr); // What a sweep must not do is disturb glyphs still in use. Measure @@ -298,7 +274,6 @@ namespace CHECK(before > 0.f); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - UNSCOPED_INFO("bitmap cache present"); REQUIRE(cache != nullptr); const U32 pages_before = cache->getNumBitmaps(EFontGlyphType::Grayscale); @@ -327,7 +302,6 @@ namespace LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* before = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("first resolve non-null"); REQUIRE(before != nullptr); // Re-call initClass with different DPI. The single-shot @@ -336,8 +310,7 @@ namespace LLFontGL::initClass(72.f, 0.5f, 0.5f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* after = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("second resolve returns same instance after re-init"); - REQUIRE(after == before); + CHECK(after == before); } // Static getter family — every named getter must resolve to a @@ -349,28 +322,17 @@ namespace SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); - UNSCOPED_INFO("SansSerif"); - REQUIRE(LLFontGL::getFontSansSerif() != nullptr); - UNSCOPED_INFO("SansSerifSmall"); - REQUIRE(LLFontGL::getFontSansSerifSmall() != nullptr); - UNSCOPED_INFO("SansSerifSmallBold"); - REQUIRE(LLFontGL::getFontSansSerifSmallBold() != nullptr); - UNSCOPED_INFO("SansSerifBig"); - REQUIRE(LLFontGL::getFontSansSerifBig() != nullptr); - UNSCOPED_INFO("SansSerifHuge"); - REQUIRE(LLFontGL::getFontSansSerifHuge() != nullptr); - UNSCOPED_INFO("SansSerifBold"); - REQUIRE(LLFontGL::getFontSansSerifBold() != nullptr); - UNSCOPED_INFO("Monospace"); - REQUIRE(LLFontGL::getFontMonospace() != nullptr); - UNSCOPED_INFO("EmojiSmall"); - REQUIRE(LLFontGL::getFontEmojiSmall() != nullptr); - UNSCOPED_INFO("EmojiMedium"); - REQUIRE(LLFontGL::getFontEmojiMedium() != nullptr); - UNSCOPED_INFO("EmojiLarge"); - REQUIRE(LLFontGL::getFontEmojiLarge() != nullptr); - UNSCOPED_INFO("FontDefault"); - REQUIRE(LLFontGL::getFontDefault() != nullptr); + CHECK(LLFontGL::getFontSansSerif() != nullptr); + CHECK(LLFontGL::getFontSansSerifSmall() != nullptr); + CHECK(LLFontGL::getFontSansSerifSmallBold() != nullptr); + CHECK(LLFontGL::getFontSansSerifBig() != nullptr); + CHECK(LLFontGL::getFontSansSerifHuge() != nullptr); + CHECK(LLFontGL::getFontSansSerifBold() != nullptr); + CHECK(LLFontGL::getFontMonospace() != nullptr); + CHECK(LLFontGL::getFontEmojiSmall() != nullptr); + CHECK(LLFontGL::getFontEmojiMedium() != nullptr); + CHECK(LLFontGL::getFontEmojiLarge() != nullptr); + CHECK(LLFontGL::getFontDefault() != nullptr); } // getWidthF32: the whole-string overload defaults to the text's length; @@ -382,7 +344,6 @@ namespace LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); const std::string s = "Hello"; const F32 wfull = font->getWidthF32(s); @@ -390,7 +351,6 @@ namespace (S32)s.size(), false); UNSCOPED_INFO("getWidthF32 with default max == explicit max"); CHECK(wfull == wexp); - UNSCOPED_INFO("width is positive"); CHECK(wfull > 0.f); } @@ -405,16 +365,12 @@ namespace LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); const F32 w1 = font->getWidthF32(std::string("A")); const F32 w2 = font->getWidthF32(std::string("AA")); const F32 w3 = font->getWidthF32(std::string("AAA")); - UNSCOPED_INFO("width(A) > 0"); CHECK(w1 > 0.f); - UNSCOPED_INFO("width(AA) > width(A)"); CHECK(w2 > w1); - UNSCOPED_INFO("width(AAA) > width(AA)"); CHECK(w3 > w2); } @@ -429,10 +385,8 @@ namespace LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); const F32 w = font->getWidthF32(std::string("....")); - UNSCOPED_INFO("ellipsis width is positive"); CHECK(w > 0.f); } @@ -445,15 +399,12 @@ namespace LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); const std::string s = "Hello"; - UNSCOPED_INFO("budget 0 fits nothing"); - REQUIRE(font->maxDrawableBytes(s, 0.f, (S32)s.size()) == + CHECK(font->maxDrawableBytes(s, 0.f, (S32)s.size()) == (S32)0); - UNSCOPED_INFO("budget F32_MAX fits the whole string"); - REQUIRE(font->maxDrawableBytes(s, F32_MAX, + CHECK(font->maxDrawableBytes(s, F32_MAX, (S32)s.size()) == (S32)s.size()); // Find a budget that should fit "Hel" but not "Hell". @@ -463,7 +414,7 @@ namespace const F32 mid_budget = (w_hel + w_hell) * 0.5f; const S32 fit = font->maxDrawableBytes(s, mid_budget, (S32)s.size()); - UNSCOPED_INFO("partial-budget fit is in [3, 3] (Hel exactly)"); + UNSCOPED_INFO("a budget halfway between Hel and Hell fits Hel"); CHECK(fit == 3); } @@ -477,7 +428,6 @@ namespace LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); const std::string s = "Hello"; // ASCII, so one byte per character: prefix lengths 0..5. @@ -523,7 +473,6 @@ namespace LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); const std::string a_s = "a"; @@ -537,7 +486,6 @@ namespace SKIP("font's fallback chain doesn't cover U+4F60"); // Tolerance: ~0.5 px accounts for sub-pixel positioning across // shape-range boundaries. - UNSCOPED_INFO("width(a你) ≈ width(a) + width(你) within 0.5px"); CHECK(std::abs(wac - (wa + wc)) <= 0.5f); } @@ -562,13 +510,10 @@ namespace for (U8 style : cases) { const std::string s = LLFontGL::getStringFromStyle(style); - UNSCOPED_INFO("string output is non-empty"); + CAPTURE(s); REQUIRE_FALSE(s.empty()); - UNSCOPED_INFO("no leading pipe"); REQUIRE(s.front() != '|'); - UNSCOPED_INFO("no trailing pipe"); REQUIRE(s.back() != '|'); - UNSCOPED_INFO("round-trip preserves style bits"); REQUIRE(LLFontGL::getStyleFromString(s) == style); } @@ -578,10 +523,8 @@ namespace CHECK(LLFontGL::getStyleFromString("FAUX-BOLD") == 0); UNSCOPED_INFO("NOTBOLD does not set BOLD"); CHECK(LLFontGL::getStyleFromString("NOTBOLD") == 0); - UNSCOPED_INFO("empty string is NORMAL"); CHECK(LLFontGL::getStyleFromString("") == LLFontGL::NORMAL); - UNSCOPED_INFO("BOLD|ITALIC tokenizes correctly"); CHECK(LLFontGL::getStyleFromString("BOLD|ITALIC") == (LLFontGL::BOLD | LLFontGL::ITALIC)); } @@ -602,7 +545,6 @@ namespace LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); // "X" + trans flag (5 codepoints) + "Y". The cluster's byte bounds @@ -644,6 +586,7 @@ namespace const S32 pos = font->byteFromPixelOffset(s, 0, x, F32_MAX, total, /*round=*/true); + CAPTURE(x, pos); const bool inside_cluster = (pos > cluster_start && pos < cluster_end); UNSCOPED_INFO("byteFromPixelOffset round=true returns mid-cluster offset"); REQUIRE_FALSE(inside_cluster); @@ -664,7 +607,6 @@ namespace LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); // Byte bounds recorded as the string is built: the five codepoints of @@ -692,6 +634,7 @@ namespace for (F32 px = 0.f; px <= wfull + 2.f; px += 0.5f) { const S32 first = font->firstDrawableByte(s, px); + CAPTURE(px, first); { INFO("firstDrawableByte names a position inside the text"); REQUIRE(first >= 0); @@ -716,7 +659,6 @@ namespace LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); // "a", e-acute (two bytes), "b", "c" -- so byte 2 is the only offset @@ -735,7 +677,8 @@ namespace for (F32 px = 0.f; px <= wfull + 2.f; px += 0.25f) { const S32 first = font->firstDrawableByte(s, px); - UNSCOPED_INFO("a larger budget starts drawing later"); + CAPTURE(px); + UNSCOPED_INFO("a larger budget starts drawing no later"); REQUIRE(first <= previous); previous = first; seen.insert(first); @@ -757,7 +700,6 @@ namespace LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); const std::string s = "Hello"; @@ -779,6 +721,7 @@ namespace // with room to spare the answer is the beginning whichever it is. for (S32 start = 0; start < total; ++start) { + CAPTURE(start); UNSCOPED_INFO("room for everything draws from the beginning"); REQUIRE(font->firstDrawableByte(s, F32_MAX, start) == (S32)0); UNSCOPED_INFO("no room draws the character asked for"); @@ -796,7 +739,6 @@ namespace LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); // Three characters, three bytes each. @@ -841,7 +783,6 @@ namespace LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); // "A", a three-byte character, "B" -- and the tail on its own. @@ -854,10 +795,10 @@ namespace CHECK(font->getWidthF32Bytes(s, 1, S32_MAX, false) == font->getWidthF32Bytes(tail, 0, S32_MAX, false)); UNSCOPED_INFO("S32_MAX from a non-zero offset does not overflow"); - REQUIRE(font->getWidthF32Bytes(s, 1, S32_MAX, false) == + CHECK(font->getWidthF32Bytes(s, 1, S32_MAX, false) == font->getWidthF32Bytes(s, 1, (S32)s.size() - 1, false)); UNSCOPED_INFO("an offset at the end measures nothing"); - REQUIRE(font->getWidthF32Bytes(s, (S32)s.size(), S32_MAX, false) == 0.f); + CHECK(font->getWidthF32Bytes(s, (S32)s.size(), S32_MAX, false) == 0.f); // Hit-testing from an offset answers in the same frame of reference. const F32 w_tail_first = font->getWidthF32Bytes(tail, 0, 3, false); @@ -885,7 +826,6 @@ namespace LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); const std::string s = "alpha beta"; @@ -942,7 +882,6 @@ namespace LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); // Long enough that a small pixel budget windows well inside it, in a @@ -970,6 +909,7 @@ namespace for (F32 px = 8.f; px < 900.f; px += 13.f) { const S32 got = font->maxDrawableBytes(s, px, total, LLFontGL::ANYWHERE); + CAPTURE(px, got); { INFO("the answer stays inside the text"); REQUIRE(got >= 0); @@ -998,6 +938,7 @@ namespace for (F32 px = 24.f; px < 900.f; px += 37.f) { const S32 got = font->maxDrawableBytes(s, px, total, style); + CAPTURE(style, px, got); { INFO("a word-boundary answer stays inside the text"); REQUIRE(got >= 0); @@ -1062,7 +1003,6 @@ namespace LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); // A chat line long enough to wrap several times at a readable width. @@ -1139,7 +1079,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); gl.clearFramebuffer(); @@ -1162,7 +1101,6 @@ namespace // Force any pending verts to draw. gGL.flush(); gGL.endList(); - UNSCOPED_INFO("render returned 1 char"); CHECK(n == 1); U32 verts = 0; @@ -1202,11 +1140,8 @@ namespace LLFontGL::RIGHT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, 3); gGL.flush(); - UNSCOPED_INFO("LEFT render returns 3 bytes"); CHECK(n_left == 3); - UNSCOPED_INFO("HCENTER render returns 3 bytes"); CHECK(n_cen == 3); - UNSCOPED_INFO("RIGHT render returns 3 bytes"); CHECK(n_right == 3); } @@ -1231,11 +1166,8 @@ namespace LLFontGL::LEFT, LLFontGL::BOTTOM, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, 2); gGL.flush(); - UNSCOPED_INFO("TOP render returns 2 bytes"); CHECK(n_top == 2); - UNSCOPED_INFO("BASELINE render returns 2 bytes"); CHECK(n_bs == 2); - UNSCOPED_INFO("BOTTOM render returns 2 bytes"); CHECK(n_btm == 2); } @@ -1259,11 +1191,8 @@ namespace LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::UNDERLINE, LLFontGL::NO_SHADOW, 5); gGL.flush(); - UNSCOPED_INFO("NORMAL render returns 5 bytes"); CHECK(n_norm == 5); - UNSCOPED_INFO("BOLD render returns 5 bytes"); CHECK(n_bold == 5); - UNSCOPED_INFO("UNDERLINE render returns 5 bytes"); CHECK(n_und == 5); } @@ -1291,11 +1220,8 @@ namespace LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::DROP_SHADOW_SOFT, 2); gGL.flush(); - UNSCOPED_INFO("NO_SHADOW returns 2"); CHECK(n_no == 2); - UNSCOPED_INFO("DROP_SHADOW returns 2"); CHECK(n_drop == 2); - UNSCOPED_INFO("DROP_SHADOW_SOFT returns 2"); CHECK(n_soft == 2); } @@ -1319,10 +1245,8 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); const LLFontFreetype* head = font->getFontFreetype(); - UNSCOPED_INFO("head freetype resolves"); REQUIRE(head != nullptr); // U+1F995 SAUROPOD — obscure enough that no other test in this @@ -1333,7 +1257,6 @@ namespace const LLFontFreetype* emoji_face = head->selectShapingFace(emoji_cp, emoji_idx); if (!emoji_face || emoji_face == head || emoji_idx == 0) SKIP("no emoji fallback covers U+1F995 in this harness"); - UNSCOPED_INFO("emoji fallback has a face wrapper"); REQUIRE(emoji_face->getFontFace() != nullptr); if (emoji_face->getFontFace()->findGlyphInfo(emoji_idx, EFontGlyphType::Color)) SKIP("U+1F995 already rasterized; mid-render upload won't fire"); @@ -1382,7 +1305,6 @@ namespace UNSCOPED_INFO("string spans two distinct atlas textures"); CHECK(latin_tex != emoji_tex); - UNSCOPED_INFO("capture produced batches"); REQUIRE_FALSE(capture.empty()); // The first captured batch is the Latin prefix, flushed when the @@ -1408,7 +1330,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); const std::string s = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E"; @@ -1456,10 +1377,8 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); const LLFontFreetype* ft = font->getFontFreetype(); - UNSCOPED_INFO("freetype present"); REQUIRE(ft != nullptr); // Devanagari, which shapes two glyphs to a cluster with an advance on @@ -1497,6 +1416,7 @@ namespace S32_MAX, (S32)px); gGL.flush(); gGL.endList(); + CAPTURE(px, n); U32 verts = 0; for (const LLVertexBufferData& entry : capture) @@ -1548,7 +1468,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); const std::string samples[] = { @@ -1562,6 +1481,7 @@ namespace for (const std::string& s : samples) { + CAPTURE(s); const S32 total = (S32)s.size(); // no_padding: right_x is where the pen ended, so it carries the // advances and not the extent overhang of the last glyph, which is @@ -1596,7 +1516,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); const std::string s = "a string rather wider than the budget it is given"; @@ -1659,13 +1578,12 @@ namespace const LLFontGL::HAlign aligns[] = { LLFontGL::LEFT, LLFontGL::RIGHT, LLFontGL::HCENTER }; for (LLFontGL* font : { LLFontGL::getFontMonospace(), LLFontGL::getFontSansSerif() }) { - UNSCOPED_INFO("font resolves"); REQUIRE(font != nullptr); for (const std::string s : { "7", "42", "12345", "Hello, world" }) { for (LLFontGL::HAlign halign : aligns) { - const std::string what = s + " aligned " + std::to_string((int)halign); + CAPTURE(s, halign); const std::vector said = frame([&] { font->renderUTF8(s, 0, 128.f, 100.f, LLColor4::white, halign, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW); }); @@ -1673,9 +1591,9 @@ namespace const F32 shift = font->placeGlyphs(s, halign, placed); const std::vector colors(placed.size(), LLColor4U(255, 255, 255, 255)); const std::vector drawn = frame([&] { font->renderGlyphs(placed.data(), colors.data(), placed.size(), 128.f + shift, 100.f); }); - UNSCOPED_INFO(what + ": drawn at all"); + UNSCOPED_INFO("drawn at all"); REQUIRE(lit(said)); - UNSCOPED_INFO(what + ": the same pixels"); + UNSCOPED_INFO("the same pixels"); REQUIRE(said == drawn); } } diff --git a/indra/llrender/tests/llfontregistry_gl_test.cpp b/indra/llrender/tests/llfontregistry_gl_test.cpp index 146911ba81..7e96537ebe 100644 --- a/indra/llrender/tests/llfontregistry_gl_test.cpp +++ b/indra/llrender/tests/llfontregistry_gl_test.cpp @@ -161,29 +161,22 @@ namespace tut SKIP("InterVariable.woff2 not present in test data dir"); return; } - UNSCOPED_INFO("parse ok"); CHECK(loadXml(kInterXml)); LLFontGL* font = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - UNSCOPED_INFO("getFont resolves Inter/Small"); REQUIRE(font != nullptr); font->generateASCIIglyphs(); const LLFontFreetype* ft = font->getFontFreetype(); - UNSCOPED_INFO("LLFontFreetype present"); REQUIRE(ft != nullptr); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - UNSCOPED_INFO("bitmap cache present"); REQUIRE(cache != nullptr); - UNSCOPED_INFO("at least one grayscale atlas page"); CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); LLImageGL* page = cache->getImageGL(EFontGlyphType::Grayscale, 0); - UNSCOPED_INFO("page 0 LLImageGL present"); REQUIRE(page != nullptr); - UNSCOPED_INFO("page 0 has a live GL texture name"); CHECK(page->getTexName() != 0); } @@ -197,17 +190,13 @@ namespace tut SKIP("InterVariable.woff2 not present in test data dir"); return; } - UNSCOPED_INFO("parse ok"); CHECK(loadXml(kInterXml)); LLFontGL* first = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - UNSCOPED_INFO("first getFont resolves"); REQUIRE(first != nullptr); LLFontGL* second = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - UNSCOPED_INFO("second getFont resolves"); REQUIRE(second != nullptr); - UNSCOPED_INFO("repeat getFont returns the same LLFontGL pointer"); CHECK(first == second); } @@ -225,26 +214,20 @@ namespace tut SKIP("InterVariable.woff2 not present in test data dir"); return; } - UNSCOPED_INFO("parse ok"); CHECK(loadXml(kInterXml)); LLFontGL* font = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - UNSCOPED_INFO("getFont resolves"); REQUIRE(font != nullptr); font->generateASCIIglyphs(); const LLFontBitmapCache* cache = font->getFontFreetype()->getFontBitmapCache(); - UNSCOPED_INFO("at least one atlas page before destroyGL"); - CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); - UNSCOPED_INFO("texname live before destroyGL"); + REQUIRE(cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); CHECK(cache->getImageGL(EFontGlyphType::Grayscale, 0)->getTexName() != 0); reg.destroyGL(); - UNSCOPED_INFO("page vector cleared after destroyGL"); CHECK((S32)cache->getNumBitmaps(EFontGlyphType::Grayscale) == 0); - UNSCOPED_INFO("registry entry survived destroyGL — same LLFontGL pointer"); CHECK(reg.getFont(LLFontDescriptor("Inter", "Small", 0)) == font); } @@ -270,13 +253,10 @@ namespace tut " " " " ""; - UNSCOPED_INFO("v1 parse ok"); CHECK(loadXml(kV1)); LLFontGL* font_v1 = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - UNSCOPED_INFO("v1 getFont resolves"); REQUIRE(font_v1 != nullptr); F32 ascender_v1 = font_v1->getAscenderHeight(); - UNSCOPED_INFO("v1 ascender > 0"); CHECK(ascender_v1 > 0.f); // Wipe + re-parse with 2x size. @@ -288,18 +268,14 @@ namespace tut " " " " ""; - UNSCOPED_INFO("v2 parse ok"); CHECK(loadXml(kV2)); LLFontGL* font_v2 = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - UNSCOPED_INFO("v2 getFont resolves"); REQUIRE(font_v2 != nullptr); F32 ascender_v2 = font_v2->getAscenderHeight(); - UNSCOPED_INFO("v2 ascender > 0"); CHECK(ascender_v2 > 0.f); // Doubling pt should ~double ascender (allow generous tolerance for // FreeType rounding: 1.7x lower bound is comfortably above 1.0x). - UNSCOPED_INFO("ascender scales with reloaded point size"); CHECK(ascender_v2 > ascender_v1 * 1.7f); } @@ -337,30 +313,24 @@ namespace tut ""; // First load: override SansSerifBase -> OpenDyslexic. - UNSCOPED_INFO("parse v1 ok"); CHECK(loadXml(kXml)); LLSD overrides; overrides["SansSerifBase"] = "OpenDyslexic"; resolve(overrides); LLFontGL* font_with = reg.getFont(LLFontDescriptor("SansSerif", "Large", 0)); - UNSCOPED_INFO("getFont with override resolves"); REQUIRE(font_with != nullptr); F32 ascender_with = font_with->getAscenderHeight(); - UNSCOPED_INFO("ascender with override > 0"); CHECK(ascender_with > 0.f); // Wipe + re-parse with no override. simulateReload(); - UNSCOPED_INFO("parse v2 ok"); CHECK(loadXml(kXml)); resolve(LLSD()); LLFontGL* font_no = reg.getFont(LLFontDescriptor("SansSerif", "Large", 0)); - UNSCOPED_INFO("getFont without override resolves"); REQUIRE(font_no != nullptr); F32 ascender_no = font_no->getAscenderHeight(); - UNSCOPED_INFO("ascender without override > 0"); CHECK(ascender_no > 0.f); // The two states must produce visibly different ascender heights: @@ -372,7 +342,6 @@ namespace tut const F32 diff = (ascender_with > ascender_no) ? (ascender_with - ascender_no) : (ascender_no - ascender_with); - UNSCOPED_INFO("override and no-override produce distinguishable ascenders"); CHECK(diff > ascender_no * 0.1f); } @@ -406,16 +375,13 @@ namespace tut " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(kXml)); resolve(); LLFontGL* font = reg.getFont(LLFontDescriptor("Inter", "Large", 0)); - UNSCOPED_INFO("getFont resolves"); REQUIRE(font != nullptr); // Head is Inter at chain's 16pt. F32 head_ascender = font->getAscenderHeight(); - UNSCOPED_INFO("head ascender > 0"); CHECK(head_ascender > 0.f); // Find the DejaVu fallback in the chain. Its mPointSize should be @@ -430,17 +396,14 @@ namespace tut break; } } - UNSCOPED_INFO("DejaVu fallback present in chain"); REQUIRE(dejavu_fallback != nullptr); F32 fb_ascender = dejavu_fallback->getAscenderHeight(); - UNSCOPED_INFO("DejaVu fallback ascender > 0"); CHECK(fb_ascender > 0.f); // The pinned fallback at 8pt must be substantially smaller than // the head's 16pt rendering. Demand >40% smaller — comfortably // separates "pin applied" from "fell through to chain pt". - UNSCOPED_INFO("pinned fallback ascender is substantially smaller than head"); CHECK(fb_ascender < head_ascender * 0.6f); } @@ -479,31 +442,25 @@ namespace tut ""; // Override A: Target -> OpenDyslexic. Per-file pin lands head at 14pt. - UNSCOPED_INFO("v1 parse ok"); CHECK(loadXml(kXml)); LLSD ovr_a; ovr_a["Target"] = "OpenDyslexic"; resolve(ovr_a); LLFontGL* font_a = reg.getFont(LLFontDescriptor("Target", "Large", 0)); - UNSCOPED_INFO("getFont under OpenDyslexic override"); REQUIRE(font_a != nullptr); F32 asc_a = font_a->getAscenderHeight(); - UNSCOPED_INFO("OpenDyslexic ascender > 0"); CHECK(asc_a > 0.f); // Swap: Target -> Inter (pin Large=8). Head must reload at 8pt // with Inter's intrinsics — substantially smaller than 14pt OD. simulateReload(); - UNSCOPED_INFO("v2 parse ok"); CHECK(loadXml(kXml)); LLSD ovr_b; ovr_b["Target"] = "Inter"; resolve(ovr_b); LLFontGL* font_b = reg.getFont(LLFontDescriptor("Target", "Large", 0)); - UNSCOPED_INFO("getFont under Inter override"); REQUIRE(font_b != nullptr); F32 asc_b = font_b->getAscenderHeight(); - UNSCOPED_INFO("Inter ascender > 0"); CHECK(asc_b > 0.f); // Inter at 8pt is comfortably smaller than OpenDyslexic at 14pt. @@ -530,16 +487,13 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(kXml)); resolve(); LLFontDescriptor desc("Phantom", "Small", 0); LLFontGL* first = reg.getFont(desc); - UNSCOPED_INFO("getFont with missing file returns NULL"); - REQUIRE(first == nullptr); + CHECK(first == nullptr); - UNSCOPED_INFO("failed createFont must NOT poison mFontMap with a NULL slot"); CHECK_FALSE(fontMapContains(desc)); } @@ -556,14 +510,11 @@ namespace tut SKIP("InterVariable.woff2 not present in test data dir"); return; } - UNSCOPED_INFO("parse ok"); CHECK(loadXml(kInterXml)); LLFontGL* font = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - UNSCOPED_INFO("getFont resolves"); REQUIRE(font != nullptr); F32 ascender_96 = font->getAscenderHeight(); - UNSCOPED_INFO("ascender at 96 DPI > 0"); CHECK(ascender_96 > 0.f); LLFontGL::sVertDPI = 192.f; @@ -571,11 +522,9 @@ namespace tut reg.reloadForDpiChange(); F32 ascender_192 = font->getAscenderHeight(); - UNSCOPED_INFO("ascender at 192 DPI > 0"); CHECK(ascender_192 > 0.f); // 192/96 = 2.0x; allow 1.7x lower bound for FreeType rounding. - UNSCOPED_INFO("ascender scales with DPI after reloadForDpiChange"); CHECK(ascender_192 > ascender_96 * 1.7f); // Restore so neighbour tests see the fixture default. @@ -596,23 +545,19 @@ namespace tut SKIP("InterVariable.woff2 not present in test data dir"); return; } - UNSCOPED_INFO("parse ok"); CHECK(loadXml(kInterXml)); LLFontGL* font = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - UNSCOPED_INFO("getFont resolves"); REQUIRE(font != nullptr); font->generateASCIIglyphs(); const LLFontBitmapCache* cache = font->getFontFreetype()->getFontBitmapCache(); const U32 pages_before = cache->getNumBitmaps(EFontGlyphType::Grayscale); - UNSCOPED_INFO("at least one atlas page before sweep"); CHECK(pages_before >= 1u); reg.sweepGlyphCaches(); const U32 pages_after = cache->getNumBitmaps(EFontGlyphType::Grayscale); - UNSCOPED_INFO("sweep does not drop in-use atlas pages"); CHECK(pages_after == pages_before); } @@ -666,15 +611,12 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(kXml)); resolve(); LLFontGL* head_a = reg.getFont(LLFontDescriptor("HeadA", "Small", 0)); LLFontGL* head_b = reg.getFont(LLFontDescriptor("HeadB", "Small", 0)); - UNSCOPED_INFO("HeadA resolves"); REQUIRE(head_a != nullptr); - UNSCOPED_INFO("HeadB resolves"); REQUIRE(head_b != nullptr); // Locate the Noto-COLRv1 fallback inside each head's chain. @@ -689,9 +631,7 @@ namespace tut }; const LLFontFreetype* emoji_a = find_emoji_fallback(head_a); const LLFontFreetype* emoji_b = find_emoji_fallback(head_b); - UNSCOPED_INFO("HeadA chain includes Noto-COLRv1"); REQUIRE(emoji_a != nullptr); - UNSCOPED_INFO("HeadB chain includes Noto-COLRv1"); REQUIRE(emoji_b != nullptr); UNSCOPED_INFO("siblings share the fallback freetype instance " "(mFallbackInstanceCache dedup)"); @@ -706,9 +646,7 @@ namespace tut kFire, EFontGlyphType::Color); const LLFontGlyphInfo* gi_b = head_b->getFontFreetype()->getGlyphInfo( kFire, EFontGlyphType::Color); - UNSCOPED_INFO("HeadA produced an emoji glyph entry"); REQUIRE(gi_a != nullptr); - UNSCOPED_INFO("HeadB produced an emoji glyph entry"); REQUIRE(gi_b != nullptr); // Face-level dedup → both heads see the same face-owned entry. UNSCOPED_INFO("face dedup returns the same entry to both heads"); @@ -728,9 +666,7 @@ namespace tut // to the deleted entry; post-fix, neither head has such a cache. const ALFontFace* emoji_face = emoji_a->getFontFace(); LLFontBitmapCache* cache = emoji_a->getBitmapCache(); - UNSCOPED_INFO("fallback freetype exposes its face"); REQUIRE(emoji_face != nullptr); - UNSCOPED_INFO("fallback freetype exposes its atlas"); REQUIRE(cache != nullptr); emoji_face->erase_glyph_entries( @@ -745,9 +681,7 @@ namespace tut return false; }); cache->releaseSheet(target.first, static_cast(target.second)); - UNSCOPED_INFO("target sheet reports released"); - CHECK(cache->isSheetReleased(target.first, - static_cast(target.second))); + CHECK(cache->isSheetReleased(target.first, static_cast(target.second))); // Re-request the emoji through both heads. Each lookup must: // 1. not crash on a freed face entry, @@ -757,17 +691,11 @@ namespace tut kFire, EFontGlyphType::Color); const LLFontGlyphInfo* gi_b2 = head_b->getFontFreetype()->getGlyphInfo( kFire, EFontGlyphType::Color); - UNSCOPED_INFO("HeadA re-rasterized after sibling-driven eviction"); REQUIRE(gi_a2 != nullptr); - UNSCOPED_INFO("HeadB re-rasterized after sibling-driven eviction"); REQUIRE(gi_b2 != nullptr); const auto post_a = gi_a2->mPhaseSlots[0].mBitmapEntry; const auto post_b = gi_b2->mPhaseSlots[0].mBitmapEntry; - UNSCOPED_INFO("HeadA's post-eviction entry points at a live sheet"); - CHECK_FALSE(cache->isSheetReleased(post_a.first, - static_cast(post_a.second))); - UNSCOPED_INFO("HeadB's post-eviction entry points at a live sheet"); - CHECK_FALSE(cache->isSheetReleased(post_b.first, - static_cast(post_b.second))); + CHECK_FALSE(cache->isSheetReleased(post_a.first, static_cast(post_a.second))); + CHECK_FALSE(cache->isSheetReleased(post_b.first, static_cast(post_b.second))); } } diff --git a/indra/llrender/tests/llfontregistry_test.cpp b/indra/llrender/tests/llfontregistry_test.cpp index 10c0279bca..ec1fcc68b5 100644 --- a/indra/llrender/tests/llfontregistry_test.cpp +++ b/indra/llrender/tests/llfontregistry_test.cpp @@ -79,9 +79,10 @@ namespace tut return init_from_xml(®, root); } - // Friend-access wrappers so test methods (which live on the derived - // tut::object class, not on this fixture) can drive the private - // resolveFontReferences / applyFamilyOverrides via inherited members. + // Friend-access wrappers so test cases (which live on a class + // TEST_CASE_METHOD derives from this fixture, not on the fixture + // itself) can drive the private resolveFontReferences / + // applyFamilyOverrides via inherited members. void resolve(const LLSD& overrides = LLSD::emptyMap()) { reg.resolveFontReferences(overrides); @@ -173,9 +174,7 @@ namespace tut { LLFontDescriptor desc("SansSerifHuge", "", 0); LLFontDescriptor n = desc.normalize(); - UNSCOPED_INFO("name keeps stem"); CHECK(n.getName() == std::string("SansSerif")); - UNSCOPED_INFO("size becomes Huge"); CHECK(n.getSize() == std::string("Huge")); UNSCOPED_INFO("style stays NORMAL"); CHECK((S32)n.getStyle() == 0); @@ -185,9 +184,7 @@ namespace tut TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: normalize rewrites the legacy size suffix Big to Large", "[LLFontRegistry]") { LLFontDescriptor n = LLFontDescriptor("SansSerifBig", "", 0).normalize(); - UNSCOPED_INFO("Big maps to Large"); CHECK(n.getSize() == std::string("Large")); - UNSCOPED_INFO("name stripped"); CHECK(n.getName() == std::string("SansSerif")); } @@ -197,11 +194,9 @@ namespace tut TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: normalize strips a Bold suffix from the name and sets the BOLD flag", "[LLFontRegistry]") { LLFontDescriptor n = LLFontDescriptor("SansSerifBold", "", 0).normalize(); - UNSCOPED_INFO("Bold stripped from name"); CHECK(n.getName() == std::string("SansSerif")); UNSCOPED_INFO("size defaults to Small (no size suffix in input)"); CHECK(n.getSize() == std::string("Small")); - UNSCOPED_INFO("BOLD flag set"); CHECK((S32)n.getStyle() == (S32)LLFontGL::BOLD); } @@ -211,9 +206,7 @@ namespace tut LLFontDescriptor desc("SansSerifBoldItalic", "", static_cast(LLFontGL::UNDERLINE)); LLFontDescriptor n = desc.normalize(); - UNSCOPED_INFO("BOLD|ITALIC flags"); - CHECK((S32)n.getStyle() == - (S32)(LLFontGL::BOLD | LLFontGL::ITALIC)); + CHECK((S32)n.getStyle() == (S32)(LLFontGL::BOLD | LLFontGL::ITALIC)); } // "Monospace" is preserved as both name and size — the special-case @@ -221,9 +214,7 @@ namespace tut TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: normalize keeps Monospace as both the name and the size", "[LLFontRegistry]") { LLFontDescriptor n = LLFontDescriptor("Monospace", "", 0).normalize(); - UNSCOPED_INFO("name preserved"); CHECK(n.getName() == std::string("Monospace")); - UNSCOPED_INFO("size becomes Monospace"); CHECK(n.getSize() == std::string("Monospace")); } @@ -231,7 +222,6 @@ namespace tut TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: normalize defaults an empty size to Small", "[LLFontRegistry]") { LLFontDescriptor n = LLFontDescriptor("SansSerif", "", 0).normalize(); - UNSCOPED_INFO("default size is Small"); CHECK(n.getSize() == std::string("Small")); } @@ -241,9 +231,7 @@ namespace tut LLFontDescriptor desc("SansSerif", "", 0); desc.addFontFile("Foo.ttf", EFontHinting::DEFAULT, 0, 0.f); LLFontDescriptor n = desc.normalize(); - UNSCOPED_INFO("file count preserved"); REQUIRE((S32)n.getFontFiles().size() == 1); - UNSCOPED_INFO("file name preserved"); CHECK(n.getFontFiles()[0].FileName == std::string("Foo.ttf")); } @@ -256,9 +244,7 @@ namespace tut LLFontDescriptor b("SansSerif", "Medium", 0); a.addFontFile("a.ttf", EFontHinting::DEFAULT, 0, 0.f); b.addFontFile("b.ttf", EFontHinting::DEFAULT, 0, 0.f); - UNSCOPED_INFO("equal regardless of files"); CHECK(a == b); - UNSCOPED_INFO("hash matches"); CHECK(hash_value(a) == hash_value(b)); } @@ -268,7 +254,6 @@ namespace tut { LLFontDescriptor a("SansSerif", "Medium", 0); LLFontDescriptor b("SansSerif", "Medium", LLFontGL::BOLD); - UNSCOPED_INFO("style breaks tie before size"); CHECK(a < b); UNSCOPED_INFO("strict weak ordering"); CHECK_FALSE(b < a); @@ -280,9 +265,7 @@ namespace tut { LLFontDescriptor t("SansSerif", "TEMPLATE", 0); LLFontDescriptor n("SansSerif", "Medium", 0); - UNSCOPED_INFO("template sentinel detected"); CHECK(t.isTemplate()); - UNSCOPED_INFO("non-template not flagged"); CHECK_FALSE(n.isTemplate()); } @@ -299,18 +282,12 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); F32 sz = 0.f; - UNSCOPED_INFO("Medium found"); CHECK(reg.nameToSize("Medium", sz)); - UNSCOPED_INFO("Medium = 12"); CHECK(sz == 12.0f); - UNSCOPED_INFO("Large found"); CHECK(reg.nameToSize("Large", sz)); - UNSCOPED_INFO("Large = 14"); CHECK(sz == 14.0f); - UNSCOPED_INFO("Huge missing"); CHECK_FALSE(reg.nameToSize("Huge", sz)); } @@ -330,28 +307,17 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); const LLFontDescriptor* normal = templateFor("Inter", LLFontGL::NORMAL); const LLFontDescriptor* bold = templateFor("Inter", LLFontGL::BOLD); - UNSCOPED_INFO("NORMAL template present"); REQUIRE(normal != nullptr); - UNSCOPED_INFO("BOLD template present"); REQUIRE(bold != nullptr); - UNSCOPED_INFO("NORMAL inherits family wght set"); CHECK(normal->getFontFiles()[0].mVarAxes.wght_set); - UNSCOPED_INFO("NORMAL inherits family weight 400"); CHECK(normal->getFontFiles()[0].mVarAxes.wght == 400.f); - UNSCOPED_INFO("NORMAL inherits family hinting light"); - CHECK((S32)normal->getFontFiles()[0].mHinting == - (S32)EFontHinting::LIGHT); - UNSCOPED_INFO("BOLD has wght set after override"); + CHECK((S32)normal->getFontFiles()[0].mHinting == (S32)EFontHinting::LIGHT); CHECK(bold->getFontFiles()[0].mVarAxes.wght_set); - UNSCOPED_INFO("BOLD overrides weight to 700"); CHECK(bold->getFontFiles()[0].mVarAxes.wght == 700.f); - UNSCOPED_INFO("BOLD inherits family hinting"); - CHECK((S32)bold->getFontFiles()[0].mHinting == - (S32)EFontHinting::LIGHT); + CHECK((S32)bold->getFontFiles()[0].mHinting == (S32)EFontHinting::LIGHT); } // children inside populate the per-family override table @@ -366,17 +332,13 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); F32 sz = 0.f; - UNSCOPED_INFO("DejaVu Medium hits family override"); CHECK(reg.nameToSize("DejaVu", "Medium", sz)); - UNSCOPED_INFO("DejaVu Medium = 8.6"); CHECK(sz == 8.6f); F32 sz2 = 0.f; UNSCOPED_INFO("Other family Medium falls back to global"); CHECK(reg.nameToSize("SansSerif", "Medium", sz2)); - UNSCOPED_INFO("Global Medium unchanged"); CHECK(sz2 == 12.0f); } @@ -391,12 +353,9 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); const LLFontDescriptor* d = templateFor("Mono", LLFontGL::NORMAL); - UNSCOPED_INFO("template present"); REQUIRE(d != nullptr); - UNSCOPED_INFO("ligatures inherited"); CHECK(d->getFontFiles()[0].mMonospaceLigatures); } @@ -413,7 +372,6 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); UNSCOPED_INFO("family-level load_collection inherited"); CHECK(templateFor("CJK")->getFontFiles()[0].mLoadCollection); @@ -434,14 +392,10 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); const LLFontDescriptor* d = templateFor("Inter", LLFontGL::BOLD); - UNSCOPED_INFO("template present"); REQUIRE(d != nullptr); - UNSCOPED_INFO("BOLD bit set on file flags"); - CHECK((d->getFontFiles()[0].mFlags & LLFontGL::BOLD) == - (S32)LLFontGL::BOLD); + CHECK((d->getFontFiles()[0].mFlags & LLFontGL::BOLD) == (S32)LLFontGL::BOLD); } // unicode_ranges parses comma-separated U+lo-U+hi tokens into a @@ -456,10 +410,8 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); const auto& cf = templateFor("Emoji")->getFontFiles()[0].CharFunctor; - UNSCOPED_INFO("functor present"); REQUIRE(static_cast(cf)); UNSCOPED_INFO("U+2600 inside range"); CHECK(cf(0x2600)); @@ -487,12 +439,9 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); const auto& cf = templateFor("Frag")->getFontFiles()[0].CharFunctor; - UNSCOPED_INFO("functor still built"); REQUIRE(static_cast(cf)); - UNSCOPED_INFO("valid range survives"); CHECK(cf(0x2700)); } @@ -508,9 +457,7 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); - UNSCOPED_INFO("size_delta preserved"); CHECK(templateFor("X")->getFontFiles()[0].mSizeDelta == -1.5f); } @@ -530,22 +477,19 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); auto fams = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::ANY); // Hidden filtered out; Inter + Mono remain. Stable sort by label. - UNSCOPED_INFO("Hidden filtered + 2 visible families"); - REQUIRE((S32)fams.size() == 2); + UNSCOPED_INFO("Hidden is filtered out: 2 visible families"); + CHECK((S32)fams.size() == 2); bool sawInter = false, sawMono = false; for (const auto& f : fams) { - if (f.name == "Inter") { sawInter = true; UNSCOPED_INFO("Inter label"); REQUIRE(f.label == std::string("Inter Display")); } + if (f.name == "Inter") { sawInter = true; REQUIRE(f.label == std::string("Inter Display")); } if (f.name == "Mono") { sawMono = true; } } - UNSCOPED_INFO("Inter present"); CHECK(sawInter); - UNSCOPED_INFO("Mono present"); CHECK(sawMono); } @@ -565,16 +509,11 @@ namespace tut " " " " ""; - UNSCOPED_INFO("base parse"); CHECK(loadXml(base)); - UNSCOPED_INFO("overlay parse"); CHECK(loadXml(overlay)); auto names = fileNames("SansSerif"); - UNSCOPED_INFO("two files merged"); REQUIRE((S32)names.size() == 2); - UNSCOPED_INFO("overlay prepended"); CHECK(names[0] == std::string("Overlay.woff2")); - UNSCOPED_INFO("base kept as fallback"); CHECK(names[1] == std::string("Base.woff2")); } @@ -596,15 +535,11 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); auto names = fileNames("A"); - UNSCOPED_INFO("A then B"); REQUIRE((S32)names.size() == 2); - UNSCOPED_INFO("A own first"); CHECK(names[0] == std::string("A.woff2")); - UNSCOPED_INFO("B appended"); CHECK(names[1] == std::string("B.woff2")); } @@ -622,15 +557,11 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); auto names = fileNames("A", LLFontGL::BOLD); - UNSCOPED_INFO("A-Bold then B-Regular fallback"); REQUIRE((S32)names.size() == 2); - UNSCOPED_INFO("A's bold first"); CHECK(names[0] == std::string("A-Bold.woff2")); - UNSCOPED_INFO("B NORMAL fallback"); CHECK(names[1] == std::string("B-Regular.woff2")); } @@ -650,16 +581,11 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); - UNSCOPED_INFO("A.woff2 once on A"); CHECK(countFile("A", "A.woff2") == 1); - UNSCOPED_INFO("B.woff2 once on A"); CHECK(countFile("A", "B.woff2") == 1); - UNSCOPED_INFO("B.woff2 once on B"); CHECK(countFile("B", "B.woff2") == 1); - UNSCOPED_INFO("A.woff2 once on B"); CHECK(countFile("B", "A.woff2") == 1); } @@ -686,14 +612,10 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); - UNSCOPED_INFO("D.woff2 appears exactly once on A"); CHECK(countFile("A", "D.woff2") == 1); - UNSCOPED_INFO("B.woff2 once on A"); CHECK(countFile("A", "B.woff2") == 1); - UNSCOPED_INFO("C.woff2 once on A"); CHECK(countFile("A", "C.woff2") == 1); } @@ -707,13 +629,10 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); - UNSCOPED_INFO("only A's own file remains"); CHECK(countFile("A", "A.woff2") == 1); - UNSCOPED_INFO("nothing else added"); - REQUIRE((S32)fileNames("A").size() == 1); + CHECK((S32)fileNames("A").size() == 1); } // targeting an undeclared family is skipped. @@ -726,11 +645,9 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); - UNSCOPED_INFO("undeclared use skipped"); - REQUIRE((S32)fileNames("A").size() == 1); + CHECK((S32)fileNames("A").size() == 1); } // -only family (no " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); - UNSCOPED_INFO("Wrapper NORMAL synthesized"); - REQUIRE(templateFor("Wrapper", LLFontGL::NORMAL) != nullptr); - UNSCOPED_INFO("Wrapper BOLD synthesized"); - REQUIRE(templateFor("Wrapper", LLFontGL::BOLD) != nullptr); + CHECK(templateFor("Wrapper", LLFontGL::NORMAL) != nullptr); + CHECK(templateFor("Wrapper", LLFontGL::BOLD) != nullptr); auto bold = fileNames("Wrapper", LLFontGL::BOLD); - UNSCOPED_INFO("Wrapper BOLD pulls Inner-B"); REQUIRE((S32)bold.size() == 1); - UNSCOPED_INFO("...and the file is Inner-B"); CHECK(bold[0] == std::string("Inner-B.woff2")); } @@ -772,15 +684,11 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); auto names = fileNames("F", LLFontGL::BOLD); - UNSCOPED_INFO("BOLD then NORMAL via inherit"); REQUIRE((S32)names.size() == 2); - UNSCOPED_INFO("BOLD first"); CHECK(names[0] == std::string("F-B.woff2")); - UNSCOPED_INFO("NORMAL appended"); CHECK(names[1] == std::string("F-R.woff2")); } @@ -804,17 +712,13 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); auto bold = fileNames("F", LLFontGL::BOLD); UNSCOPED_INFO("3 files post-dedup: own bold + B from + NORMAL inherit"); REQUIRE((S32)bold.size() == 3); - UNSCOPED_INFO("F-B first"); CHECK(bold[0] == std::string("F-B.woff2")); - UNSCOPED_INFO("B from "); CHECK(bold[1] == std::string("B.woff2")); - UNSCOPED_INFO("F-R inherited"); CHECK(bold[2] == std::string("F-R.woff2")); UNSCOPED_INFO("duplicate B from NORMAL's deduped out"); CHECK(countFile("F", "B.woff2", LLFontGL::BOLD) == 1); @@ -836,14 +740,12 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); auto first = fileNames("A"); resolve(); // second call must not re-append auto second = fileNames("A"); - UNSCOPED_INFO("size unchanged across re-resolve"); - REQUIRE((S32)first.size() == (S32)second.size()); + CHECK((S32)first.size() == (S32)second.size()); } // =================================================================== @@ -864,18 +766,14 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); LLSD overrides; overrides["UI"] = "Alt"; applyOverrides(overrides); auto names = fileNames("UI"); - UNSCOPED_INFO("source prepended + target preserved"); REQUIRE((S32)names.size() == 2); - UNSCOPED_INFO("Alt prepended"); CHECK(names[0] == std::string("Alt.woff2")); - UNSCOPED_INFO("UI fallback kept"); CHECK(names[1] == std::string("UI.woff2")); } @@ -891,21 +789,15 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); LLSD overrides; overrides["UI"] = "user-supplied.ttf"; applyOverrides(overrides); const auto& files = templateFor("UI")->getFontFiles(); - UNSCOPED_INFO("file added in front"); - CHECK(files[0].FileName == - std::string("user-supplied.ttf")); - UNSCOPED_INFO("hinting inherited from target's first file"); + CHECK(files[0].FileName == std::string("user-supplied.ttf")); CHECK((S32)files[0].mHinting == (S32)EFontHinting::LIGHT); - UNSCOPED_INFO("weight inherited"); CHECK(files[0].mVarAxes.wght == 400.f); - UNSCOPED_INFO("ligatures inherited"); CHECK(files[0].mMonospaceLigatures); } @@ -919,15 +811,13 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); LLSD overrides; overrides["UI"] = "UI"; applyOverrides(overrides); auto names = fileNames("UI"); - UNSCOPED_INFO("self-override skipped"); - REQUIRE((S32)names.size() == 1); + CHECK((S32)names.size() == 1); } // Unknown target family: skipped, no phantom entries created. @@ -939,16 +829,14 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); LLSD overrides; overrides["Phantom"] = "Real"; applyOverrides(overrides); - UNSCOPED_INFO("no template materialized for Phantom"); - REQUIRE(templateFor("Phantom") == nullptr); + CHECK(templateFor("Phantom") == nullptr); UNSCOPED_INFO("Real untouched"); - REQUIRE((S32)fileNames("Real").size() == 1); + CHECK((S32)fileNames("Real").size() == 1); } // overridesEqual: snapshot of last-applied overrides updates with @@ -961,16 +849,13 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); LLSD ovr1; ovr1["UI"] = "Alt"; resolve(ovr1); - UNSCOPED_INFO("identical maps compare equal"); CHECK(reg.overridesEqual(ovr1)); LLSD ovr2; ovr2["UI"] = "Different"; - UNSCOPED_INFO("different value differs"); CHECK_FALSE(reg.overridesEqual(ovr2)); } @@ -994,7 +879,6 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); LLSD overrides; @@ -1010,11 +894,8 @@ namespace tut // mSourceFamily=Alt — so per-file pin lookup at render time // would use Alt's for that prepended file. const auto& files = templateFor("UI")->getFontFiles(); - UNSCOPED_INFO("UI chain has at least one file"); REQUIRE_FALSE(files.empty()); - UNSCOPED_INFO("first file = Alt's prepended file"); CHECK(files[0].FileName == std::string("A.woff2")); - UNSCOPED_INFO("prepended file tagged with source family Alt"); CHECK(files[0].mSourceFamily == std::string("Alt")); // Alt's pin is intact for per-file lookup. F32 alt_pin = 0.f; @@ -1044,15 +925,12 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); LLFontDescriptor query("SansSerif", "TEMPLATE", 0); - UNSCOPED_INFO("template lookup hits"); - REQUIRE(reg.getMatchingFontDesc(query) != nullptr); + CHECK(reg.getMatchingFontDesc(query) != nullptr); LLFontDescriptor miss("Nope", "TEMPLATE", 0); - UNSCOPED_INFO("absent name misses"); - REQUIRE(reg.getMatchingFontDesc(miss) == nullptr); + CHECK(reg.getMatchingFontDesc(miss) == nullptr); } // getClosestFontTemplate prefers the entry whose style bits overlap @@ -1067,15 +945,12 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); // Asking for BOLD: exact match wins. LLFontDescriptor want_bold("F", "TEMPLATE", LLFontGL::BOLD); const LLFontDescriptor* best = reg.getClosestFontTemplate(want_bold); - UNSCOPED_INFO("bold exact"); REQUIRE(best != nullptr); - UNSCOPED_INFO("bold style"); CHECK((S32)best->getStyle() == (S32)LLFontGL::BOLD); // Asking for BOLD|ITALIC with no BOLD|ITALIC entry: the BOLD @@ -1083,7 +958,7 @@ namespace tut LLFontDescriptor want_bi("F", "TEMPLATE", (U8)(LLFontGL::BOLD | LLFontGL::ITALIC)); best = reg.getClosestFontTemplate(want_bi); - UNSCOPED_INFO("BI fallback hit"); + UNSCOPED_INFO("BOLD|ITALIC request falls back to a template"); REQUIRE(best != nullptr); UNSCOPED_INFO("BOLD wins tiebreak"); CHECK((S32)best->getStyle() == (S32)LLFontGL::BOLD); @@ -1100,12 +975,10 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); LLFontDescriptor want_normal("F", "TEMPLATE", 0); - UNSCOPED_INFO("no NORMAL match returned"); - REQUIRE(reg.getClosestFontTemplate(want_normal) == nullptr); + CHECK(reg.getClosestFontTemplate(want_normal) == nullptr); } // Empty-family overload of nameToSize skips the per-family path @@ -1120,12 +993,9 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); F32 sz = 0.f; - UNSCOPED_INFO("empty-family overload finds Medium"); CHECK(reg.nameToSize("Medium", sz)); - UNSCOPED_INFO("...and gets the global value"); CHECK(sz == 12.0f); F32 sz2 = 0.f; reg.nameToSize("F", "Medium", sz2); @@ -1156,15 +1026,12 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); auto fams = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::ANY); - UNSCOPED_INFO("only 2 selectable families"); REQUIRE((S32)fams.size() == 2); UNSCOPED_INFO("sorted by label, Apple first"); CHECK(fams[0].name == std::string("Apple")); - UNSCOPED_INFO("Zebra second"); CHECK(fams[1].name == std::string("Zebra")); } @@ -1181,18 +1048,13 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); auto mono = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::MONOSPACE); - UNSCOPED_INFO("MONOSPACE: 1 family"); REQUIRE((S32)mono.size() == 1); - UNSCOPED_INFO("MONOSPACE returns Mono"); CHECK(mono[0].name == std::string("Mono")); auto prop = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::PROPORTIONAL); - UNSCOPED_INFO("PROPORTIONAL: 1 family"); REQUIRE((S32)prop.size() == 1); - UNSCOPED_INFO("PROPORTIONAL returns Sans"); CHECK(prop[0].name == std::string("Sans")); } @@ -1206,18 +1068,15 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); auto fams = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::ANY); - UNSCOPED_INFO("one family"); REQUIRE((S32)fams.size() == 1); - UNSCOPED_INFO("label defaults to name"); CHECK(fams[0].label == std::string("Plain")); } // inside — literal self-reference - // by name, distinct from the 'self' alias path (test 26). The cycle + // by name, distinct from the 'self' alias path (Group 3). The cycle // detector must skip both. The post-resolve chain should contain // only A's own files, with no duplicate insertion. TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a naming the family itself leaves only the family's own file", "[LLFontRegistry]") @@ -1229,13 +1088,10 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); auto names = fileNames("Mono"); - UNSCOPED_INFO("self-reference by literal name doesn't duplicate files"); REQUIRE((S32)names.size() == 1); - UNSCOPED_INFO("only the family's own file in chain"); CHECK(names[0] == std::string("M.woff2")); } @@ -1247,12 +1103,10 @@ namespace tut TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an empty root parses and resolves to no families", "[LLFontRegistry]") { const char* xml = ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); auto fams = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::ANY); - UNSCOPED_INFO("empty XML produces empty available-families list"); - REQUIRE((S32)fams.size() == 0); + CHECK((S32)fams.size() == 0); } // applyOverrides called twice with different LLSD maps: the second @@ -1279,7 +1133,6 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); @@ -1288,11 +1141,8 @@ namespace tut ovr1["UI"] = "AltA"; applyOverrides(ovr1); const auto& files_a = templateFor("UI")->getFontFiles(); - UNSCOPED_INFO("UI chain non-empty after first override"); REQUIRE_FALSE(files_a.empty()); - UNSCOPED_INFO("UI head = AltA's file"); CHECK(files_a[0].FileName == std::string("A.woff2")); - UNSCOPED_INFO("head's mSourceFamily = AltA"); CHECK(files_a[0].mSourceFamily == std::string("AltA")); // Second override: UI → AltB. The earlier UI→AltA mapping must @@ -1301,11 +1151,8 @@ namespace tut ovr2["UI"] = "AltB"; applyOverrides(ovr2); const auto& files_b = templateFor("UI")->getFontFiles(); - UNSCOPED_INFO("UI chain non-empty after second override"); REQUIRE_FALSE(files_b.empty()); - UNSCOPED_INFO("UI head = AltB's file (replaced AltA)"); CHECK(files_b[0].FileName == std::string("B.woff2")); - UNSCOPED_INFO("head's mSourceFamily = AltB"); CHECK(files_b[0].mSourceFamily == std::string("AltB")); } @@ -1351,7 +1198,6 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); LLSD overrides; @@ -1359,27 +1205,19 @@ namespace tut resolve(overrides); // Direct target picks up the override. - UNSCOPED_INFO("Base[0] is the override file"); - REQUIRE((fileNames("Base").size() > 0u - ? fileNames("Base")[0] - : std::string()) == - std::string("Custom.woff2")); + CHECK((fileNames("Base").size() > 0u ? fileNames("Base")[0] : std::string()) + == std::string("Custom.woff2")); // Both dependents see Custom.woff2 in their chain — that's the // load-bearing assertion. A regression to the old order // (overrides applied after resolution) would leave them // without Custom.woff2 entirely. - UNSCOPED_INFO("Plain chain includes Custom.woff2 via Base"); CHECK(countFile("Plain", "Custom.woff2") >= 1); - UNSCOPED_INFO("Menu chain includes Custom.woff2 via Base"); CHECK(countFile("Menu", "Custom.woff2") >= 1); // The chain after the override-driven Base files must still // carry Source (Base's own ) and the sibling s — the // override prepends, doesn't replace. - UNSCOPED_INFO("Plain chain still has Source.woff2"); CHECK(countFile("Plain", "Source.woff2") >= 1); - UNSCOPED_INFO("Plain chain still has Latin.woff2"); CHECK(countFile("Plain", "Latin.woff2") >= 1); - UNSCOPED_INFO("Menu chain still has Mark.woff2"); CHECK(countFile("Menu", "Mark.woff2") >= 1); // Position pin: the override file MUST be at index 0 of every @@ -1389,17 +1227,11 @@ namespace tut // somewhere later than 0, the original Source.woff2 still wins // for every glyph it covers and the user observes "no change." const auto plain_files = fileNames("Plain"); - UNSCOPED_INFO("Plain chain non-empty"); - REQUIRE_FALSE(plain_files.empty()); - UNSCOPED_INFO("Plain[0] must be the override file"); - REQUIRE((plain_files.empty() ? std::string() : plain_files[0]) == - std::string("Custom.woff2")); + CHECK_FALSE(plain_files.empty()); + CHECK((plain_files.empty() ? std::string() : plain_files[0]) == std::string("Custom.woff2")); const auto menu_files = fileNames("Menu"); - UNSCOPED_INFO("Menu chain non-empty"); - REQUIRE_FALSE(menu_files.empty()); - UNSCOPED_INFO("Menu[0] must be the override file"); - REQUIRE((menu_files.empty() ? std::string() : menu_files[0]) == - std::string("Custom.woff2")); + CHECK_FALSE(menu_files.empty()); + CHECK((menu_files.empty() ? std::string() : menu_files[0]) == std::string("Custom.woff2")); } // Override propagation through chains for BOLD style. Real @@ -1430,7 +1262,6 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); LLSD overrides; @@ -1439,16 +1270,12 @@ namespace tut // BOLD variant of Menu must lead with the override's BOLD file. const auto menu_bold = fileNames("Menu", LLFontGL::BOLD); - UNSCOPED_INFO("Menu BOLD chain non-empty"); - REQUIRE_FALSE(menu_bold.empty()); - UNSCOPED_INFO("Menu BOLD[0] must be override BOLD file"); - REQUIRE((menu_bold.empty() ? std::string() : menu_bold[0]) == - std::string("Custom-Bold.woff2")); + CHECK_FALSE(menu_bold.empty()); + CHECK((menu_bold.empty() ? std::string() : menu_bold[0]) == std::string("Custom-Bold.woff2")); // NORMAL variant unchanged invariant. const auto menu_normal = fileNames("Menu", LLFontGL::NORMAL); - UNSCOPED_INFO("Menu NORMAL[0] must be override NORMAL file"); - REQUIRE((menu_normal.empty() ? std::string() : menu_normal[0]) == - std::string("Custom-Regular.woff2")); + CHECK((menu_normal.empty() ? std::string() : menu_normal[0]) + == std::string("Custom-Regular.woff2")); } // Reload with a different override: first resolve applies override A, @@ -1480,48 +1307,34 @@ namespace tut ""; // First load: override Base -> AltA. - UNSCOPED_INFO("first parse ok"); CHECK(loadXml(xml)); LLSD ovr_a; ovr_a["Base"] = "AltA"; resolve(ovr_a); const auto menu_after_a = fileNames("Menu"); - UNSCOPED_INFO("Menu chain non-empty after override A"); - REQUIRE_FALSE(menu_after_a.empty()); - UNSCOPED_INFO("Menu[0] is AltA after first override"); - REQUIRE((menu_after_a.empty() ? std::string() : menu_after_a[0]) == - std::string("AltA.woff2")); - UNSCOPED_INFO("Menu chain has Source after first override"); + CHECK_FALSE(menu_after_a.empty()); + CHECK((menu_after_a.empty() ? std::string() : menu_after_a[0]) == std::string("AltA.woff2")); CHECK(countFile("Menu", "Source.woff2") >= 1); // Simulate reload: wipe parse-time state, re-parse, apply // override B. AltA must be GONE from the chain — a regression // would leak the prior override into the new resolution. simulateReload(); - UNSCOPED_INFO("re-parse ok after simulated reload"); CHECK(loadXml(xml)); LLSD ovr_b; ovr_b["Base"] = "AltB"; resolve(ovr_b); const auto menu_after_b = fileNames("Menu"); - UNSCOPED_INFO("Menu chain non-empty after override B"); - REQUIRE_FALSE(menu_after_b.empty()); - UNSCOPED_INFO("Menu[0] is AltB after second override"); - REQUIRE((menu_after_b.empty() ? std::string() : menu_after_b[0]) == - std::string("AltB.woff2")); - UNSCOPED_INFO("AltA must NOT appear in Menu chain after replace"); + CHECK_FALSE(menu_after_b.empty()); + CHECK((menu_after_b.empty() ? std::string() : menu_after_b[0]) == std::string("AltB.woff2")); CHECK(countFile("Menu", "AltA.woff2") == 0); - UNSCOPED_INFO("Menu chain still has Source after replace"); CHECK(countFile("Menu", "Source.woff2") >= 1); // Same checks for the direct target — Base itself must show // only the new override at chain[0], no residue from AltA. const auto base_after_b = fileNames("Base"); - UNSCOPED_INFO("Base[0] is AltB after second override"); - REQUIRE((base_after_b.empty() ? std::string() : base_after_b[0]) == - std::string("AltB.woff2")); - UNSCOPED_INFO("AltA must NOT appear in Base chain after replace"); + CHECK((base_after_b.empty() ? std::string() : base_after_b[0]) == std::string("AltB.woff2")); CHECK(countFile("Base", "AltA.woff2") == 0); } @@ -1547,29 +1360,21 @@ namespace tut " " ""; - UNSCOPED_INFO("first parse ok"); CHECK(loadXml(xml)); LLSD ovr_a; ovr_a["Base"] = "AltA"; resolve(ovr_a); - UNSCOPED_INFO("Menu[0] is AltA before clear"); - REQUIRE((fileNames("Menu").empty() - ? std::string() - : fileNames("Menu")[0]) == - std::string("AltA.woff2")); + CHECK((fileNames("Menu").empty() ? std::string() : fileNames("Menu")[0]) + == std::string("AltA.woff2")); // Reload with empty overrides — picker's "(default)" sentinel. simulateReload(); - UNSCOPED_INFO("re-parse ok"); CHECK(loadXml(xml)); resolve(LLSD::emptyMap()); const auto menu = fileNames("Menu"); - UNSCOPED_INFO("Menu non-empty after clear"); REQUIRE_FALSE(menu.empty()); - UNSCOPED_INFO("Menu[0] returns to Source after override clear"); CHECK(menu[0] == std::string("Source.woff2")); - UNSCOPED_INFO("AltA gone from Menu after clear"); CHECK(countFile("Menu", "AltA.woff2") == 0); } @@ -1599,7 +1404,6 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); LLSD overrides; @@ -1608,24 +1412,20 @@ namespace tut // Without dedup, Menu would be [DejaVu(override), Source, // DejaVu(), …]. With dedup, the second DejaVu is dropped. - UNSCOPED_INFO("Menu chain has DejaVu exactly once"); CHECK(countFile("Menu", "DejaVu.woff2") == 1); - UNSCOPED_INFO("Menu chain still has Source"); CHECK(countFile("Menu", "Source.woff2") >= 1); const auto menu = fileNames("Menu"); - UNSCOPED_INFO("Menu non-empty"); REQUIRE_FALSE(menu.empty()); - UNSCOPED_INFO("Menu[0] is the override file"); CHECK(menu[0] == std::string("DejaVu.woff2")); } // Pure diamond, no override. A reaches D twice — once through // B and once through C — and the dedup pass at the end of - // resolveFontReferences must drop the second copy. Test 52 covers - // the override + shape; this one pins that the dedup logic - // also operates on plain chain composition with no override - // involved, so a future regression that gates dedup on "did - // applyFamilyOverrides run?" would fail here. + // resolveFontReferences must drop the second copy. The dedup pin + // above covers the override + shape; this one pins that the + // dedup logic also operates on plain chain composition with no + // override involved, so a future regression that gates dedup on + // "did applyFamilyOverrides run?" would fail here. TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a plain diamond with no override is deduplicated too", "[LLFontRegistry]") { const char* xml = @@ -1644,18 +1444,15 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); // A reaches D twice (via B and via C). Pre-dedup the chain // would be [D, D]; post-dedup [D] — first hit wins, second // copy dropped. - UNSCOPED_INFO("A chain has D exactly once"); CHECK(countFile("A", "D.woff2") == 1); const auto a_files = fileNames("A"); - UNSCOPED_INFO("A chain length post-dedup"); - REQUIRE((S32)a_files.size() == 1); + CHECK((S32)a_files.size() == 1); } // Two functored entries with the same filename must NOT collapse. @@ -1691,7 +1488,6 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); @@ -1699,7 +1495,6 @@ namespace tut // CharFunctor. Dedup must keep both — collapsing would route // every codepoint through whichever functor survived, breaking // coverage for the dropped range. - UNSCOPED_INFO("Combined keeps both functored entries"); CHECK(countFile("Combined", "Shared.ttf") == 2); } @@ -1717,7 +1512,6 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse v1 ok"); CHECK(loadXml(xml_v1)); resolve(); F32 sz = 0.f; @@ -1738,7 +1532,6 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse v2 ok"); CHECK(loadXml(xml_v2)); resolve(); CHECK(reg.nameToSize("Inter", "Foo", sz)); @@ -1770,7 +1563,6 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); F32 sz = 0.f; @@ -1802,7 +1594,6 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); // Baseline: SansSerifBase has no , falls to global. @@ -1836,15 +1627,12 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); F32 a_sz = 0.f, b_sz = 0.f; CHECK(familySizePt("A", "Large", a_sz)); CHECK(familySizePt("B", "Large", b_sz)); - UNSCOPED_INFO("A Large pin = 8.0"); CHECK(a_sz == 8.0f); - UNSCOPED_INFO("B Large pin = 13.0"); CHECK(b_sz == 13.0f); } @@ -1867,20 +1655,14 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); const auto& a_files = templateFor("A")->getFontFiles(); - UNSCOPED_INFO("A chain has 2 files"); REQUIRE((S32)a_files.size() == 2); - UNSCOPED_INFO("A's own file tagged 'A'"); CHECK(a_files[0].mSourceFamily == std::string("A")); - UNSCOPED_INFO("B's file tagged 'B' through "); CHECK(a_files[1].mSourceFamily == std::string("B")); F32 b_sz = 0.f; - UNSCOPED_INFO("B's pin still in mFamilySizes"); CHECK(familySizePt("B", "Large", b_sz)); - UNSCOPED_INFO("B's pin = 8.0"); CHECK(b_sz == 8.0f); } @@ -1899,12 +1681,10 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse v1 ok"); CHECK(loadXml(xml_v1)); resolve(); F32 v1 = 0.f; CHECK(familySizePt("B", "Large", v1)); - UNSCOPED_INFO("v1: B Large pin = 8.0"); CHECK(v1 == 8.0f); // Edit and reload — the new pin should land in mFamilySizes. @@ -1917,12 +1697,10 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse v2 ok"); CHECK(loadXml(xml_v2)); resolve(); F32 v2 = 0.f; CHECK(familySizePt("B", "Large", v2)); - UNSCOPED_INFO("v2: B Large pin = 13.5 (hot-reloaded)"); CHECK(v2 == 13.5f); } @@ -1944,13 +1722,10 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); // Empty-name template not created. The well-formed sibling is. - UNSCOPED_INFO("nameless not registered"); - REQUIRE(templateFor("", LLFontGL::NORMAL) == nullptr); - UNSCOPED_INFO("well-formed sibling parsed"); - REQUIRE(templateFor("Good", LLFontGL::NORMAL) != nullptr); + CHECK(templateFor("", LLFontGL::NORMAL) == nullptr); + CHECK(templateFor("Good", LLFontGL::NORMAL) != nullptr); } // missing either attribute is silently dropped (parser @@ -1963,14 +1738,10 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); F32 sz = 0.f; - UNSCOPED_INFO("OnlyName not added"); CHECK_FALSE(reg.nameToSize("OnlyName", sz)); - UNSCOPED_INFO("Both registered"); CHECK(reg.nameToSize("Both", sz)); - UNSCOPED_INFO("Both = 13.0"); CHECK(sz == 13.0f); } @@ -1989,15 +1760,11 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); auto names = fileNames("A"); - UNSCOPED_INFO("A own + Target via well-formed "); REQUIRE((S32)names.size() == 2); - UNSCOPED_INFO("A own first"); CHECK(names[0] == std::string("A.woff2")); - UNSCOPED_INFO("Target second"); CHECK(names[1] == std::string("Target.woff2")); } @@ -2015,14 +1782,12 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); F32 sz = 0.f; CHECK(reg.nameToSize("F", "Foo", sz)); UNSCOPED_INFO("F Foo = own 8.0 (well-formed entry takes effect)"); CHECK(sz == 8.0f); - UNSCOPED_INFO("Malformed name 'OnlyName' did not register on F"); CHECK_FALSE(reg.nameToSize("F", "OnlyName", sz)); } @@ -2045,14 +1810,11 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); - UNSCOPED_INFO("'WEIRD' style mapped to NORMAL"); - REQUIRE(templateFor("Quirky", LLFontGL::NORMAL) != nullptr); - UNSCOPED_INFO("no BOLD template synthesized for unrecognized style"); - REQUIRE(templateFor("Quirky", LLFontGL::BOLD) == nullptr); + CHECK(templateFor("Quirky", LLFontGL::NORMAL) != nullptr); + CHECK(templateFor("Quirky", LLFontGL::BOLD) == nullptr); } // Three-level cycle (A -> B -> C -> A). Visited set must @@ -2112,19 +1870,14 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); // Each chain should contain exactly one of each file, in the // walk order starting from the queried family. - UNSCOPED_INFO("A: A.woff2 once"); CHECK(countFile("A", "A.woff2") == 1); - UNSCOPED_INFO("A: B.woff2 once"); CHECK(countFile("A", "B.woff2") == 1); - UNSCOPED_INFO("A: C.woff2 once"); CHECK(countFile("A", "C.woff2") == 1); - UNSCOPED_INFO("A chain length 3"); - REQUIRE((S32)fileNames("A").size() == 3); + CHECK((S32)fileNames("A").size() == 3); } // Deep linear chain (5 levels). Walker descends without depth @@ -2153,21 +1906,14 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); auto names = fileNames("F1"); - UNSCOPED_INFO("F1 chain has all 5 files"); REQUIRE((S32)names.size() == 5); - UNSCOPED_INFO("F1 own first"); CHECK(names[0] == std::string("F1.woff2")); - UNSCOPED_INFO("F2 next"); CHECK(names[1] == std::string("F2.woff2")); - UNSCOPED_INFO("F3 next"); CHECK(names[2] == std::string("F3.woff2")); - UNSCOPED_INFO("F4 next"); CHECK(names[3] == std::string("F4.woff2")); - UNSCOPED_INFO("F5 last"); CHECK(names[4] == std::string("F5.woff2")); } @@ -2184,13 +1930,10 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); auto bold = fileNames("Lonely", LLFontGL::BOLD); - UNSCOPED_INFO("BOLD has only its own file (no NORMAL to inherit)"); REQUIRE((S32)bold.size() == 1); - UNSCOPED_INFO("Lonely-Bold present"); CHECK(bold[0] == std::string("Lonely-Bold.woff2")); } @@ -2216,23 +1959,15 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); - UNSCOPED_INFO("A NORMAL synthesized (NORMAL declared in chain)"); - REQUIRE(templateFor("A", LLFontGL::NORMAL) != nullptr); - UNSCOPED_INFO("A BOLD NOT synthesized (no BOLD in chain)"); - REQUIRE(templateFor("A", LLFontGL::BOLD) == nullptr); - UNSCOPED_INFO("A ITALIC NOT synthesized (no ITALIC in chain)"); - REQUIRE(templateFor("A", LLFontGL::ITALIC) == nullptr); - UNSCOPED_INFO("A BOLD|ITALIC NOT synthesized"); - REQUIRE(templateFor("A", - static_cast(LLFontGL::BOLD | LLFontGL::ITALIC)) - == nullptr); + CHECK(templateFor("A", LLFontGL::NORMAL) != nullptr); + CHECK(templateFor("A", LLFontGL::BOLD) == nullptr); + CHECK(templateFor("A", LLFontGL::ITALIC) == nullptr); + CHECK(templateFor("A", static_cast(LLFontGL::BOLD | LLFontGL::ITALIC)) == nullptr); // NORMAL synthesizes from B and C: auto normal = fileNames("A", LLFontGL::NORMAL); - UNSCOPED_INFO("A NORMAL pulls both fallbacks"); - REQUIRE((S32)normal.size() == 2); + CHECK((S32)normal.size() == 2); } // Override that points to a use-only family: applyFamilyOverrides @@ -2254,7 +1989,6 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); LLSD overrides; @@ -2262,9 +1996,7 @@ namespace tut applyOverrides(overrides); auto names = fileNames("Target"); // Override prepends Composite's resolved chain (= Inner's files). - UNSCOPED_INFO("Target chain after override is non-empty"); REQUIRE_FALSE(names.empty()); - UNSCOPED_INFO("Inner.woff2 prepended via Composite override"); CHECK(names[0] == std::string("Inner.woff2")); UNSCOPED_INFO("Target's own file kept as fallback behind override"); CHECK(countFile("Target", "Target.woff2") == 1); @@ -2285,25 +2017,20 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); // entries land in mFamilySizes. F32 large = 0.f, medium = 0.f; CHECK(familySizePt("Pinned", "Large", large)); - UNSCOPED_INFO("Large = 8.0"); CHECK(large == 8.0f); CHECK(familySizePt("Pinned", "Medium", medium)); - UNSCOPED_INFO("Medium = 7.0"); CHECK(medium == 7.0f); // Files contributed by Pinned's " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); const auto& files = templateFor("A")->getFontFiles(); - UNSCOPED_INFO("A chain length 2"); REQUIRE((S32)files.size() == 2); - UNSCOPED_INFO("A's own file tagged 'A'"); CHECK(files[0].mSourceFamily == std::string("A")); - UNSCOPED_INFO("B's file tagged 'B' (preserved through )"); CHECK(files[1].mSourceFamily == std::string("B")); } @@ -2368,17 +2091,13 @@ namespace tut ""; // Boot state: override Target -> SourceA via reload-equivalent. - UNSCOPED_INFO("parse v1 ok"); CHECK(loadXml(xml)); LLSD ovr_a; ovr_a["Target"] = "SourceA"; resolve(ovr_a); const auto& a_files = templateFor("Target")->getFontFiles(); - UNSCOPED_INFO("Target chain non-empty after first override"); REQUIRE_FALSE(a_files.empty()); - UNSCOPED_INFO("Target head = SourceA's file"); CHECK(a_files[0].FileName == std::string("SourceA.woff2")); - UNSCOPED_INFO("head's mSourceFamily = SourceA"); CHECK(a_files[0].mSourceFamily == std::string("SourceA")); // Picker swap: production calls reload() which wipes mFontMap + @@ -2386,21 +2105,16 @@ namespace tut // mirrors the wipe; loadXml + resolve mirrors the re-parse + // re-resolve. simulateReload(); - UNSCOPED_INFO("parse v2 ok"); CHECK(loadXml(xml)); LLSD ovr_b; ovr_b["Target"] = "SourceB"; resolve(ovr_b); const auto& b_files = templateFor("Target")->getFontFiles(); - UNSCOPED_INFO("Target chain non-empty after swap"); REQUIRE_FALSE(b_files.empty()); - UNSCOPED_INFO("Target head = SourceB's file (clean swap)"); CHECK(b_files[0].FileName == std::string("SourceB.woff2")); - UNSCOPED_INFO("head's mSourceFamily = SourceB"); CHECK(b_files[0].mSourceFamily == std::string("SourceB")); // After the reload-driven swap, SourceA's file is gone — no // leftover state from the prior override. - UNSCOPED_INFO("SourceA's file is not in the post-swap chain"); CHECK(countFile("Target", "SourceA.woff2") == 0); // Both source families' pins remain queryable in mFamilySizes @@ -2409,10 +2123,8 @@ namespace tut // under ovr_b that hits SourceB's 14. F32 a_pin = 0.f, b_pin = 0.f; CHECK(familySizePt("SourceA", "Large", a_pin)); - UNSCOPED_INFO("SourceA pin = 10.0"); CHECK(a_pin == 10.0f); CHECK(familySizePt("SourceB", "Large", b_pin)); - UNSCOPED_INFO("SourceB pin = 14.0"); CHECK(b_pin == 14.0f); // Target's own nameToSize is unaffected by either override — @@ -2444,17 +2156,12 @@ namespace tut LLFontDescriptor a("SansSerif", "Small", 0, files_a); LLFontDescriptor b("SansSerif", "Small", 0, files_b); - UNSCOPED_INFO("same (name,style,size) compares equal regardless of files"); CHECK(a == b); - UNSCOPED_INFO("same (name,style,size) hashes the same regardless of files"); CHECK(hash_value(a) == hash_value(b)); // Sanity: vary each component, equality breaks. - UNSCOPED_INFO("different name is not equal"); CHECK_FALSE(a == LLFontDescriptor("Other", "Small", 0, files_a)); - UNSCOPED_INFO("different style is not equal"); CHECK_FALSE(a == LLFontDescriptor("SansSerif", "Small", LLFontGL::BOLD, files_a)); - UNSCOPED_INFO("different size is not equal"); CHECK_FALSE(a == LLFontDescriptor("SansSerif", "Large", 0, files_a)); // Lookup by no-files descriptor must hit a stored with-files entry. @@ -2463,12 +2170,11 @@ namespace tut m[a] = 1; LLFontDescriptor lookup("SansSerif", "Small", 0); // no files auto it = m.find(lookup); - UNSCOPED_INFO("lookup by no-files descriptor finds a stored with-files entry"); - REQUIRE(it != m.end()); + CHECK(it != m.end()); } - // overridesEqual: extra coverage beyond test<36>'s identical/different - // value pair. Pin the LLSD-comparison semantics initClass relies on + // overridesEqual: extra coverage beyond the identical/different value + // pair in Group 4. Pin the LLSD-comparison semantics initClass relies on // when deciding fast-DPI vs full-reload: empty-map and undefined-LLSD // are distinct values, and map equality is order-independent. TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: overridesEqual tells empty from undefined and ignores key order", "[LLFontRegistry]") @@ -2477,18 +2183,15 @@ namespace tut "" " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); // Resolve with empty-map; mLastFontOverrides becomes empty-map. LLSD empty_map = LLSD::emptyMap(); resolve(empty_map); - UNSCOPED_INFO("empty map equals empty map"); CHECK(reg.overridesEqual(empty_map)); // Undefined LLSD is distinct from empty-map per llsd_equals. LLSD undef; - UNSCOPED_INFO("empty map differs from undefined LLSD"); CHECK_FALSE(reg.overridesEqual(undef)); // Map equality is keyed by (key,value) pairs regardless of LLSD's @@ -2496,7 +2199,6 @@ namespace tut // (e.g. picker rebuilds the map in different order) doesn't trip // the fast-path and force a redundant full reload. simulateReload(); - UNSCOPED_INFO("re-parse ok"); CHECK(loadXml(xml)); LLSD ovr1; ovr1["UI"] = "Alt"; @@ -2505,7 +2207,6 @@ namespace tut ovr2["Other"] = "Pick"; ovr2["UI"] = "Alt"; resolve(ovr1); - UNSCOPED_INFO("LLSD map equality is order-independent"); CHECK(reg.overridesEqual(ovr2)); } @@ -2521,12 +2222,10 @@ namespace tut "" " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); // No heads instantiated, no fallback-cache entries. reg.sweepGlyphCaches(); - UNSCOPED_INFO("registry survives sweep with templates only"); CHECK(true); } @@ -2546,44 +2245,28 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); const LLFontDescriptor* normal = templateFor("Demo", LLFontGL::NORMAL); - UNSCOPED_INFO("NORMAL template present"); REQUIRE(normal != nullptr); const auto& nfiles = normal->getFontFiles(); - UNSCOPED_INFO("NORMAL has one file"); REQUIRE(nfiles.size() == 1u); - UNSCOPED_INFO("NORMAL inherits family ital"); CHECK(nfiles[0].mVarAxes.ital_set); - UNSCOPED_INFO("NORMAL ital == 1.0"); CHECK(nfiles[0].mVarAxes.ital == 1.0f); - UNSCOPED_INFO("NORMAL inherits family wdth"); CHECK(nfiles[0].mVarAxes.wdth_set); - UNSCOPED_INFO("NORMAL wdth == 87.5"); CHECK(nfiles[0].mVarAxes.wdth == 87.5f); - UNSCOPED_INFO("NORMAL inherits family slnt"); CHECK(nfiles[0].mVarAxes.slnt_set); - UNSCOPED_INFO("NORMAL slnt == -12.0"); CHECK(nfiles[0].mVarAxes.slnt == -12.0f); // Per-file override on wdth wins; ital/slnt still inherit. const LLFontDescriptor* bold = templateFor("Demo", LLFontGL::BOLD); - UNSCOPED_INFO("BOLD template present"); REQUIRE(bold != nullptr); const auto& wfiles = bold->getFontFiles(); - UNSCOPED_INFO("BOLD has one file"); REQUIRE(wfiles.size() == 1u); - UNSCOPED_INFO("BOLD overrides wdth to 125.0"); CHECK(wfiles[0].mVarAxes.wdth == 125.0f); - UNSCOPED_INFO("BOLD keeps inherited ital"); CHECK(wfiles[0].mVarAxes.ital_set); - UNSCOPED_INFO("BOLD inherited ital still 1.0"); CHECK(wfiles[0].mVarAxes.ital == 1.0f); - UNSCOPED_INFO("BOLD keeps inherited slnt"); CHECK(wfiles[0].mVarAxes.slnt_set); - UNSCOPED_INFO("BOLD inherited slnt still -12.0"); CHECK(wfiles[0].mVarAxes.slnt == -12.0f); } @@ -2598,21 +2281,14 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); const LLFontDescriptor* d = templateFor("Plain", LLFontGL::NORMAL); - UNSCOPED_INFO("template present"); REQUIRE(d != nullptr); const auto& f = d->getFontFiles()[0]; - UNSCOPED_INFO("no wght"); CHECK_FALSE(f.mVarAxes.wght_set); - UNSCOPED_INFO("no opsz"); CHECK_FALSE(f.mVarAxes.opsz_set); - UNSCOPED_INFO("no ital"); CHECK_FALSE(f.mVarAxes.ital_set); - UNSCOPED_INFO("no wdth"); CHECK_FALSE(f.mVarAxes.wdth_set); - UNSCOPED_INFO("no slnt"); CHECK_FALSE(f.mVarAxes.slnt_set); } @@ -2632,35 +2308,26 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); const LLFontDescriptor* normal = templateFor("Demo", LLFontGL::NORMAL); - UNSCOPED_INFO("NORMAL template present"); REQUIRE(normal != nullptr); const auto& nfiles = normal->getFontFiles(); - UNSCOPED_INFO("NORMAL has one file"); REQUIRE(nfiles.size() == 1u); - UNSCOPED_INFO("NORMAL inherits family opsz"); CHECK(nfiles[0].mVarAxes.opsz_set); - UNSCOPED_INFO("NORMAL opsz == 14.0"); CHECK(nfiles[0].mVarAxes.opsz == 14.0f); const LLFontDescriptor* bold = templateFor("Demo", LLFontGL::BOLD); - UNSCOPED_INFO("BOLD template present"); REQUIRE(bold != nullptr); const auto& bfiles = bold->getFontFiles(); - UNSCOPED_INFO("BOLD has one file"); REQUIRE(bfiles.size() == 1u); - UNSCOPED_INFO("BOLD has opsz set after override"); CHECK(bfiles[0].mVarAxes.opsz_set); - UNSCOPED_INFO("BOLD overrides opsz to 28.0"); CHECK(bfiles[0].mVarAxes.opsz == 28.0f); } // replace_first=true on a family→family override drops the target's - // head file before prepending the source. Mirror of test<32> with - // the new map shape. + // head file before prepending the source. Mirror of Group 4's + // family→family override case with the new map shape. TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: replace_first drops the target's head before prepending the source", "[LLFontRegistry]") { const char* xml = @@ -2672,7 +2339,6 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); LLSD overrides; @@ -2682,9 +2348,7 @@ namespace tut overrides["UI"] = entry; applyOverrides(overrides); auto names = fileNames("UI"); - UNSCOPED_INFO("head replaced -> only one file remains"); REQUIRE((S32)names.size() == 1); - UNSCOPED_INFO("Alt is the head"); CHECK(names[0] == std::string("Alt.woff2")); } @@ -2705,14 +2369,11 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); // Sanity: post-resolve UI's chain is [UI.woff2, DejaVu.woff2]. auto pre_names = fileNames("UI"); - UNSCOPED_INFO("UI starts with 2 files"); REQUIRE((S32)pre_names.size() == 2); - UNSCOPED_INFO("UI head is UI.woff2"); CHECK(pre_names[0] == std::string("UI.woff2")); LLSD overrides; @@ -2723,11 +2384,8 @@ namespace tut applyOverrides(overrides); auto names = fileNames("UI"); - UNSCOPED_INFO("[Alt, DejaVu] -> 2 files"); REQUIRE((S32)names.size() == 2); - UNSCOPED_INFO("Alt is the head"); CHECK(names[0] == std::string("Alt.woff2")); - UNSCOPED_INFO("DejaVu fallback preserved"); CHECK(names[1] == std::string("DejaVu.woff2")); } @@ -2746,7 +2404,6 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); @@ -2758,11 +2415,8 @@ namespace tut applyOverrides(overrides); auto names = fileNames("UI"); - UNSCOPED_INFO("[user.ttf, DejaVu] -> 2 files"); REQUIRE((S32)names.size() == 2); - UNSCOPED_INFO("user.ttf head"); CHECK(names[0] == std::string("user.ttf")); - UNSCOPED_INFO("DejaVu fallback preserved"); CHECK(names[1] == std::string("DejaVu.woff2")); } @@ -2781,7 +2435,6 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); @@ -2793,11 +2446,8 @@ namespace tut applyOverrides(overrides); auto names = fileNames("UI"); - UNSCOPED_INFO("map(replace_first=false) -> [Alt, UI]"); REQUIRE((S32)names.size() == 2); - UNSCOPED_INFO("Alt prepended"); CHECK(names[0] == std::string("Alt.woff2")); - UNSCOPED_INFO("UI head preserved as fallback (no replace)"); CHECK(names[1] == std::string("UI.woff2")); } @@ -2814,18 +2464,14 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); LLSD overrides; overrides["UI"] = "Alt"; // bare string applyOverrides(overrides); auto names = fileNames("UI"); - UNSCOPED_INFO("string shape -> [Alt, UI]"); REQUIRE((S32)names.size() == 2); - UNSCOPED_INFO("Alt prepended"); CHECK(names[0] == std::string("Alt.woff2")); - UNSCOPED_INFO("UI head preserved as fallback"); CHECK(names[1] == std::string("UI.woff2")); } @@ -2846,24 +2492,16 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); const LLFontDescriptor* d = templateFor("Consumer"); - UNSCOPED_INFO("template present"); REQUIRE(d != nullptr); const auto& files = d->getFontFiles(); - UNSCOPED_INFO("Consumer has 2 files"); REQUIRE((S32)files.size() == 2); - UNSCOPED_INFO("UI head"); CHECK(files[0].FileName == std::string("UI.woff2")); - UNSCOPED_INFO("Noto from EmojiBase"); CHECK(files[1].FileName == std::string("Noto.ttf")); - UNSCOPED_INFO("Noto has a use-level functor"); REQUIRE(static_cast(files[1].CharFunctor)); - UNSCOPED_INFO("functor accepts U+1F600"); CHECK(files[1].CharFunctor((llwchar)0x1F600)); - UNSCOPED_INFO("functor rejects ASCII"); CHECK_FALSE(files[1].CharFunctor((llwchar)0x0041)); } @@ -2883,14 +2521,11 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); const LLFontDescriptor* d = templateFor("Consumer"); - UNSCOPED_INFO("template present"); REQUIRE(d != nullptr); const auto& files = d->getFontFiles(); - UNSCOPED_INFO("Consumer has 1 file"); REQUIRE((S32)files.size() == 1); UNSCOPED_INFO("composed functor accepts U+1F500 (in both)"); CHECK(files[0].CharFunctor((llwchar)0x1F500)); @@ -2918,25 +2553,18 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); const auto& bf = templateFor("Broad")->getFontFiles(); const auto& nf = templateFor("Narrow")->getFontFiles(); - UNSCOPED_INFO("Broad has 1 file"); REQUIRE((S32)bf.size() == 1); - UNSCOPED_INFO("Narrow has 1 file"); REQUIRE((S32)nf.size() == 1); // Broad accepts U+1F100; Narrow doesn't. - UNSCOPED_INFO("Broad accepts U+1F100"); CHECK(bf[0].CharFunctor((llwchar)0x1F100)); - UNSCOPED_INFO("Narrow rejects U+1F100"); CHECK_FALSE(nf[0].CharFunctor((llwchar)0x1F100)); // Both accept U+1F600 (in their own range). - UNSCOPED_INFO("Broad accepts U+1F600"); CHECK(bf[0].CharFunctor((llwchar)0x1F600)); - UNSCOPED_INFO("Narrow accepts U+1F600"); CHECK(nf[0].CharFunctor((llwchar)0x1F600)); } @@ -2958,7 +2586,6 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); // Override at the SOURCE — EmojiBase gets prepended with Twemoji. @@ -2975,18 +2602,12 @@ namespace tut // Each consumer sees BOTH files (Twemoji prepended onto the // EmojiBase chain by the override; Noto preserved behind it), // each with the consumer's filter applied. - UNSCOPED_INFO("Broad has 2 files (Twemoji + Noto)"); REQUIRE((S32)bf.size() == 2); - UNSCOPED_INFO("Broad head is Twemoji"); CHECK(bf[0].FileName == std::string("Twemoji.ttf")); - UNSCOPED_INFO("Narrow has 2 files"); REQUIRE((S32)nf.size() == 2); - UNSCOPED_INFO("Narrow head is Twemoji"); CHECK(nf[0].FileName == std::string("Twemoji.ttf")); // Filter still gates each consumer independently. - UNSCOPED_INFO("Broad's Twemoji accepts U+1F100"); CHECK(bf[0].CharFunctor((llwchar)0x1F100)); - UNSCOPED_INFO("Narrow's Twemoji rejects U+1F100"); CHECK_FALSE(nf[0].CharFunctor((llwchar)0x1F100)); } @@ -3008,7 +2629,6 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); LLSD overrides; @@ -3022,23 +2642,14 @@ namespace tut const auto& nf = templateFor("Narrow")->getFontFiles(); // replace_first dropped Noto from EmojiBase's chain; consumers // pull just Twemoji through their respective use-level filters. - UNSCOPED_INFO("Broad has 1 file (Noto displaced)"); REQUIRE((S32)bf.size() == 1); - UNSCOPED_INFO("Broad head is Twemoji"); CHECK(bf[0].FileName == std::string("Twemoji.ttf")); - UNSCOPED_INFO("Narrow has 1 file (Noto displaced)"); REQUIRE((S32)nf.size() == 1); - UNSCOPED_INFO("Narrow head is Twemoji"); CHECK(nf[0].FileName == std::string("Twemoji.ttf")); - UNSCOPED_INFO("Broad's Twemoji accepts U+1F100"); CHECK(bf[0].CharFunctor((llwchar)0x1F100)); - UNSCOPED_INFO("Narrow's Twemoji rejects U+1F100"); CHECK_FALSE(nf[0].CharFunctor((llwchar)0x1F100)); - { - INFO("Both accept U+1F600"); - CHECK(bf[0].CharFunctor((llwchar)0x1F600)); - CHECK(nf[0].CharFunctor((llwchar)0x1F600)); - } + CHECK(bf[0].CharFunctor((llwchar)0x1F600)); + CHECK(nf[0].CharFunctor((llwchar)0x1F600)); } // No-cascade regression: a use-level filter applies ONLY to the @@ -3062,17 +2673,14 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); const LLFontDescriptor* d = templateFor("Outer"); - UNSCOPED_INFO("Outer template present"); REQUIRE(d != nullptr); const auto& files = d->getFontFiles(); // Outer pulls Mid's own files (Mid.ttf) PLUS Mid's chain // (Inner.ttf). The filter applies only to Mid's direct files. - UNSCOPED_INFO("Outer has 2 files"); - REQUIRE((S32)files.size() == 2); + CHECK((S32)files.size() == 2); // Locate the entries by filename rather than positional index — // collect_chain orders direct files before use-chain files @@ -3085,22 +2693,16 @@ namespace tut if (f.FileName == "Mid.ttf") mid = &f; if (f.FileName == "Inner.ttf") inner = &f; } - UNSCOPED_INFO("Mid.ttf contributed"); REQUIRE(mid != nullptr); - UNSCOPED_INFO("Inner.ttf contributed"); REQUIRE(inner != nullptr); // Mid is filtered (use-level filter applied to its own files). - UNSCOPED_INFO("Mid has functor"); REQUIRE(static_cast(mid->CharFunctor)); - UNSCOPED_INFO("Mid functor accepts U+1F600"); CHECK(mid->CharFunctor((llwchar)0x1F600)); - UNSCOPED_INFO("Mid functor rejects U+0041"); CHECK_FALSE(mid->CharFunctor((llwchar)0x0041)); // Inner is NOT filtered — the use-level filter doesn't cascade. - UNSCOPED_INFO("Inner has no functor (no cascade)"); - REQUIRE_FALSE(static_cast(inner->CharFunctor)); + CHECK_FALSE(static_cast(inner->CharFunctor)); } // emoji="true" attribute parses; getAvailableFamilies(EMOJI) returns @@ -3124,34 +2726,26 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); // EMOJI returns the two emoji families, sorted by label. auto em = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::EMOJI); - UNSCOPED_INFO("EMOJI: 2 families"); - REQUIRE((S32)em.size() == 2); + CHECK((S32)em.size() == 2); std::set em_names; for (const auto& f : em) em_names.insert(f.name); - UNSCOPED_INFO("EMOJI returns NotoEmoji"); - REQUIRE(em_names.count("NotoEmoji") == 1); - UNSCOPED_INFO("EMOJI returns Twemoji"); - REQUIRE(em_names.count("Twemoji") == 1); + CHECK(em_names.count("NotoEmoji") == 1); + CHECK(em_names.count("Twemoji") == 1); // PROPORTIONAL excludes both monospace AND emoji. auto prop = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::PROPORTIONAL); - UNSCOPED_INFO("PROPORTIONAL: 1 family (just Sans)"); REQUIRE((S32)prop.size() == 1); - UNSCOPED_INFO("PROPORTIONAL returns Sans"); CHECK(prop[0].name == std::string("Sans")); // MONOSPACE excludes emoji families even if they were also // marked monospace=true (emoji wins for filter routing). auto mono = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::MONOSPACE); - UNSCOPED_INFO("MONOSPACE: 1 family (just Mono)"); REQUIRE((S32)mono.size() == 1); - UNSCOPED_INFO("MONOSPACE returns Mono"); CHECK(mono[0].name == std::string("Mono")); } @@ -3166,15 +2760,12 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); auto em = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::EMOJI); - UNSCOPED_INFO("EMOJI returns the dual-flagged family"); - REQUIRE((S32)em.size() == 1); + CHECK((S32)em.size() == 1); auto mono = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::MONOSPACE); - UNSCOPED_INFO("MONOSPACE excludes the dual-flagged family"); - REQUIRE((S32)mono.size() == 0); + CHECK((S32)mono.size() == 0); } // user_selectable="false" still hides a family from EMOJI just like @@ -3192,13 +2783,10 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); auto em = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::EMOJI); - UNSCOPED_INFO("EMOJI: only the user-selectable family"); REQUIRE((S32)em.size() == 1); - UNSCOPED_INFO("EMOJI returns Public"); CHECK(em[0].name == std::string("Public")); } @@ -3206,7 +2794,7 @@ namespace tut // by Emoji with a unicode_ranges filter. Emoji's resolved chain // contains exactly one Noto file (contributed via the deepest // step, with no filter — the use-level range is dropped on - // recursion per the explicit design rule, see test<94>). + // recursion per the explicit design rule, see the no-cascade case above). TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a filter on a use-only family leaves the deeper file unfiltered", "[LLFontRegistry]") { const char* xml = @@ -3221,16 +2809,12 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); resolve(); const auto& files = templateFor("Emoji")->getFontFiles(); - UNSCOPED_INFO("Emoji has exactly 1 file (baseline)"); REQUIRE((S32)files.size() == 1); - UNSCOPED_INFO("the file is Noto.ttf"); CHECK(files[0].FileName == std::string("Noto.ttf")); - UNSCOPED_INFO("Noto fallback has NO functor (recursion dropped use-level filter)"); - REQUIRE_FALSE(static_cast(files[0].CharFunctor)); + CHECK_FALSE(static_cast(files[0].CharFunctor)); } // Override target already s the override source: a use-only @@ -3266,7 +2850,6 @@ namespace tut " " " " ""; - UNSCOPED_INFO("parse ok"); CHECK(loadXml(xml)); LLSD overrides; LLSD entry = LLSD::emptyMap(); @@ -3276,8 +2859,6 @@ namespace tut resolve(overrides); const auto& files = templateFor("Emoji")->getFontFiles(); - UNSCOPED_INFO("Emoji chain length unchanged by no-op override " - "(EmojiBase already s NotoEmoji)"); - REQUIRE((S32)files.size() == 1); + CHECK((S32)files.size() == 1); } } diff --git a/indra/llrender/tests/llfonttextcache_test.cpp b/indra/llrender/tests/llfonttextcache_test.cpp index c431141f35..bae46234e5 100644 --- a/indra/llrender/tests/llfonttextcache_test.cpp +++ b/indra/llrender/tests/llfonttextcache_test.cpp @@ -126,7 +126,6 @@ namespace TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: a default-constructed cache destroys cleanly", "[LLFontTextCache]") { LLFontTextCache vb; - UNSCOPED_INFO("default-constructed buffer reaches end-of-scope"); CHECK(true); // Implicit destruct at scope exit; assertion above catches any // crash during dtor by being unreached. @@ -138,7 +137,6 @@ namespace LLFontTextCache vb; vb.reset(); vb.reset(); // idempotent - UNSCOPED_INFO("reset on fresh buffer returns cleanly"); CHECK(true); } @@ -151,7 +149,6 @@ namespace LLFontTextCache::enableBufferCollection(true); LLFontTextCache::enableColorOnlyRegen(false); LLFontTextCache::enableColorOnlyRegen(true); - UNSCOPED_INFO("static toggles set/reset without crash"); CHECK(true); } @@ -161,7 +158,6 @@ namespace LLFontTextCache wb; wb.reset(); wb.reset(); - UNSCOPED_INFO("LLFontTextCache construct + reset return cleanly"); CHECK(true); LLFontTextCache::enableBufferCollection(false); LLFontTextCache::enableBufferCollection(true); @@ -180,7 +176,6 @@ namespace b.reset(); w1.reset(); w2.reset(); - UNSCOPED_INFO("multiple coexisting buffers reset cleanly"); CHECK(true); } @@ -197,7 +192,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font available"); REQUIRE(font != nullptr); const std::string s = "Hello"; @@ -205,11 +199,10 @@ namespace const F32 first = wb.getWidthBytes(font, s, 0, 5, false); const F32 second = wb.getWidthBytes(font, s, 0, 5, false); const F32 ref = font->getWidthF32Bytes(s, 0, 5, false); - UNSCOPED_INFO("first width matches getWidthF32"); + UNSCOPED_INFO("first width matches getWidthF32Bytes"); CHECK(first == ref); UNSCOPED_INFO("second call same as first (cache hit)"); CHECK(second == first); - UNSCOPED_INFO("width is positive"); CHECK(first > 0.f); } @@ -221,14 +214,12 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font available"); REQUIRE(font != nullptr); const std::string s = "Hello"; LLFontTextCache wb; const F32 w_5 = wb.getWidthBytes(font, s, 0, 5, false); const F32 w_3 = wb.getWidthBytes(font, s, 0, 3, false); - UNSCOPED_INFO("width(5) > width(3)"); CHECK(w_5 > w_3); const F32 w_3_again = wb.getWidthBytes(font, s, 0, 3, false); UNSCOPED_INFO("identical max_chars hits cache"); @@ -242,7 +233,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font available"); REQUIRE(font != nullptr); const std::string s = "Hi"; @@ -268,7 +258,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font available"); REQUIRE(font != nullptr); const std::string s = "Hi"; @@ -296,7 +285,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font available"); REQUIRE(font != nullptr); const std::string s = "Hi"; @@ -320,7 +308,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font available"); REQUIRE(font != nullptr); const std::string s = "Hi"; @@ -342,7 +329,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font available"); REQUIRE(font != nullptr); // "A", a three-byte character, "B": five bytes, three characters. @@ -357,6 +343,7 @@ namespace LLFontTextCache wb; for (S32 bound : { 0, 1, 4, 5 }) { + CAPTURE(bound); const F32 cached = wb.getWidthBytes(font, s, 0, bound, false); UNSCOPED_INFO("cached width agrees with the font"); REQUIRE(cached == font->getWidthF32Bytes(s, 0, bound, false)); @@ -398,14 +385,12 @@ namespace // Same length, differing at the front and at the back -- the second is // what a sample taken only from the front would miss. ALFontCacheKey front; - UNSCOPED_INFO("records"); REQUIRE(front.sameTextAsRecorded("Received Items")); UNSCOPED_INFO("a different string of the same length is caught"); CHECK_FALSE(front.sameTextAsRecorded("Xeceived Items")); ALFontCacheKey back; const std::string common(64, 'a'); - UNSCOPED_INFO("records"); REQUIRE(back.sameTextAsRecorded(common + "tail")); UNSCOPED_INFO("a difference past the sampled front is caught"); CHECK_FALSE(back.sameTextAsRecorded(common + "tai1")); @@ -450,7 +435,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font available"); REQUIRE(font != nullptr); const std::string s = "Hello"; @@ -462,7 +446,6 @@ namespace LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, /*max_chars=*/5); - UNSCOPED_INFO("render returned 5 chars"); CHECK(n == 5); } @@ -476,7 +459,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font available"); REQUIRE(font != nullptr); const std::string s = "Hello"; @@ -510,7 +492,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font available"); REQUIRE(font != nullptr); const std::string s = "Hello"; @@ -540,7 +521,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font available"); REQUIRE(font != nullptr); const std::string s = "AB"; @@ -555,7 +535,6 @@ namespace LLFontGL::NORMAL, LLFontGL::NO_SHADOW, 2); UNSCOPED_INFO("char count stable after geometry change"); CHECK(n1 == n2); - UNSCOPED_INFO("both calls returned positive count"); CHECK(n1 > 0); } @@ -568,7 +547,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font available"); REQUIRE(font != nullptr); const std::string s = "Hi"; @@ -597,7 +575,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font available"); REQUIRE(font != nullptr); const std::string s = "Hi"; @@ -611,7 +588,6 @@ namespace LLFontGL::BOLD, LLFontGL::NO_SHADOW, 2); UNSCOPED_INFO("char count stable across style flip"); CHECK(n_norm == n_bold); - UNSCOPED_INFO("both renders returned positive"); CHECK(n_norm > 0); } @@ -626,7 +602,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font available"); REQUIRE(font != nullptr); ll_test::FontStateScope scope; @@ -679,7 +654,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font available"); REQUIRE(font != nullptr); ll_test::FontStateScope scope; @@ -718,7 +692,7 @@ namespace /*use_ellipses=*/true, true); UNSCOPED_INFO("full-fit render returns the entire string length"); - REQUIRE(n_full == (S32)s.length()); + CHECK(n_full == (S32)s.length()); } // Moving text does not rebuild it. Glyph geometry is built relative to the @@ -731,7 +705,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font available"); REQUIRE(font != nullptr); ll_test::FontStateScope scope; @@ -807,7 +780,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font available"); REQUIRE(font != nullptr); ll_test::FontStateScope scope; @@ -877,7 +849,6 @@ namespace if (!fileExists(kFontsXml)) SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - UNSCOPED_INFO("font available"); REQUIRE(font != nullptr); ll_test::FontStateScope scope; diff --git a/indra/llrender/tests/llglslshader_gl_test.cpp b/indra/llrender/tests/llglslshader_gl_test.cpp index b5681fa521..dea89e7cf8 100644 --- a/indra/llrender/tests/llglslshader_gl_test.cpp +++ b/indra/llrender/tests/llglslshader_gl_test.cpp @@ -642,15 +642,13 @@ namespace void setUpShaders() { UNSCOPED_INFO("the test's names follow the product's"); - REQUIRE(mShaders.mReservedUniforms.size() == - size_t(LLShaderMgr::END_RESERVED_UNIFORMS)); + REQUIRE(mShaders.mReservedUniforms.size() == size_t(LLShaderMgr::END_RESERVED_UNIFORMS)); for (const char* name : kTestReservedNames) { mShaders.mReservedUniforms.push_back(name); } UNSCOPED_INFO("the test's blocks follow the product's"); - REQUIRE(mShaders.mReservedStorageBlocks.size() == - size_t(LLShaderMgr::END_RESERVED_STORAGE_BLOCKS)); + REQUIRE(mShaders.mReservedStorageBlocks.size() == size_t(LLShaderMgr::END_RESERVED_STORAGE_BLOCKS)); for (const auto& block : kTestStorageBlocks) { mShaders.mReservedStorageBlocks.push_back({ block.first, block.second }); @@ -658,7 +656,7 @@ namespace for (const auto& object : kStubObjects) { S32 level = 1; - UNSCOPED_INFO(std::string("compiled ") + object.first); + CAPTURE(object.first); REQUIRE(mShaders.loadShaderFile(object.first, level, object.second) != 0); } } @@ -697,9 +695,7 @@ namespace const bool linked = mShader.mapAttributes(); glDeleteShader(vs); glDeleteShader(fs); - UNSCOPED_INFO("program links"); REQUIRE(linked); - UNSCOPED_INFO("program maps"); REQUIRE(mShader.mapUniforms()); mShader.bind(); } @@ -738,42 +734,45 @@ namespace // What the program holds at a uniform, as GL reads it back, against what a check expects of its first // components. Floats are compared by their bits, so a check is exact. - void ensureInts(const std::string& what, const char* name, std::initializer_list expected) const + void require_ints(const std::string& what, const char* name, std::initializer_list expected) const { + INFO(what); std::array held = {}; glGetUniformiv(mShader.mProgramObject, location(name), held.data()); - size_t i = 0; + size_t component = 0; for (const GLint value : expected) { - UNSCOPED_INFO(what + ", component " + std::to_string(i)); - REQUIRE(held[i] == value); - ++i; + CAPTURE(component); + REQUIRE(held[component] == value); + ++component; } } - void ensureUints(const std::string& what, const char* name, std::initializer_list expected) const + void require_uints(const std::string& what, const char* name, std::initializer_list expected) const { + INFO(what); std::array held = {}; glGetUniformuiv(mShader.mProgramObject, location(name), held.data()); - size_t i = 0; + size_t component = 0; for (const GLuint value : expected) { - UNSCOPED_INFO(what + ", component " + std::to_string(i)); - REQUIRE(held[i] == value); - ++i; + CAPTURE(component); + REQUIRE(held[component] == value); + ++component; } } - void ensureFloats(const std::string& what, const char* name, std::initializer_list expected) const + void require_floats(const std::string& what, const char* name, std::initializer_list expected) const { + INFO(what); std::array held = {}; glGetUniformfv(mShader.mProgramObject, location(name), held.data()); - size_t i = 0; + size_t component = 0; for (const F32 value : expected) { - UNSCOPED_INFO(what + ", component " + std::to_string(i) + " bits"); - REQUIRE(std::bit_cast(held[i]) == std::bit_cast(value)); - ++i; + CAPTURE(component); + REQUIRE(std::bit_cast(held[component]) == std::bit_cast(value)); + ++component; } } @@ -801,30 +800,29 @@ namespace mShader.uniform1i(LLShaderMgr::MULTI_LIGHT_COUNT, BIG); mShader.uniform1i(LLShaderMgr::MULTI_LIGHT_COUNT, BIG + 1); - ensureInts("int", "light_count", { BIG + 1 }); + require_ints("int", "light_count", { BIG + 1 }); const GLint first[4] = { BIG, -BIG, BIG, BIG }; const GLint second[4] = { BIG, -BIG, BIG, BIG + 1 }; const GLint third[4] = { BIG + 1, -BIG - 1, BIG, BIG + 1 }; mShader.uniform4iv(LLShaderMgr::SUN_UP_FACTOR, 1, first); - ensureInts("ivec4", "sun_up_factor", { BIG, -BIG, BIG, BIG }); + require_ints("ivec4", "sun_up_factor", { BIG, -BIG, BIG, BIG }); mShader.uniform4iv(LLShaderMgr::SUN_UP_FACTOR, 1, second); - ensureInts("ivec4, w alone changed", "sun_up_factor", { BIG, -BIG, BIG, BIG + 1 }); + require_ints("ivec4, w alone changed", "sun_up_factor", { BIG, -BIG, BIG, BIG + 1 }); mShader.uniform4iv(LLShaderMgr::SUN_UP_FACTOR, 1, third); - ensureInts("ivec4, x and y changed", "sun_up_factor", { BIG + 1, -BIG - 1, BIG, BIG + 1 }); + require_ints("ivec4, x and y changed", "sun_up_factor", { BIG + 1, -BIG - 1, BIG, BIG + 1 }); mShader.uniform1ui(LLShaderMgr::FRAME_ID, (GLuint)BIG); mShader.uniform1ui(LLShaderMgr::FRAME_ID, (GLuint)BIG + 1); - ensureUints("uint", "uFrameId", { (GLuint)BIG + 1 }); + require_uints("uint", "uFrameId", { (GLuint)BIG + 1 }); // The sharpening constants are floats' bits. 1.0's and the next float up's are both 1065353216 as floats. const GLuint one[4] = { 0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u }; const GLuint next[4] = { 0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800001u }; mShader.uniform4uiv(LLShaderMgr::CAS_PARAM_0, 1, one); mShader.uniform4uiv(LLShaderMgr::CAS_PARAM_0, 1, next); - ensureUints("uvec4, w alone changed", "cas_param_0", { 0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800001u }); + require_uints("uvec4, w alone changed", "cas_param_0", { 0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800001u }); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -836,52 +834,51 @@ namespace mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 1.f); mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 0.5f); - ensureFloats("vec4 B", "color", { 0.25f, 0.5f, 0.75f, 0.5f }); + require_floats("vec4 B", "color", { 0.25f, 0.5f, 0.75f, 0.5f }); const F32 color[4] = { 0.25f, 0.5f, 0.75f, 1.f }; mShader.uniform4fv(LLShaderMgr::DIFFUSE_COLOR, 1, color); - ensureFloats("vec4 A again", "color", { 0.25f, 0.5f, 0.75f, 1.f }); + require_floats("vec4 A again", "color", { 0.25f, 0.5f, 0.75f, 1.f }); mShader.uniform3f(LLShaderMgr::DEFERRED_SUN_DIR, 0.f, 0.5f, 1.f); const F32 sun[3] = { 0.f, 0.5f, 0.25f }; mShader.uniform3fv(LLShaderMgr::DEFERRED_SUN_DIR, 1, sun); - ensureFloats("vec3 B", "sun_dir", { 0.f, 0.5f, 0.25f }); + require_floats("vec3 B", "sun_dir", { 0.f, 0.5f, 0.25f }); mShader.uniform3f(LLShaderMgr::DEFERRED_SUN_DIR, 0.f, 0.5f, 1.f); - ensureFloats("vec3 A again", "sun_dir", { 0.f, 0.5f, 1.f }); + require_floats("vec3 A again", "sun_dir", { 0.f, 0.5f, 1.f }); mShader.uniform2f(LLShaderMgr::DEFERRED_SCREEN_RES, 640.f, 480.f); const F32 res[2] = { 640.f, 360.f }; mShader.uniform2fv(LLShaderMgr::DEFERRED_SCREEN_RES, 1, res); - ensureFloats("vec2 B", "screen_res", { 640.f, 360.f }); + require_floats("vec2 B", "screen_res", { 640.f, 360.f }); mShader.uniform2f(LLShaderMgr::DEFERRED_SCREEN_RES, 640.f, 480.f); - ensureFloats("vec2 A again", "screen_res", { 640.f, 480.f }); + require_floats("vec2 A again", "screen_res", { 640.f, 480.f }); mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.25f); const F32 intensity = 0.5f; mShader.uniform1fv(LLShaderMgr::ENVIRONMENT_INTENSITY, 1, &intensity); - ensureFloats("float B", "env_intensity", { 0.5f }); + require_floats("float B", "env_intensity", { 0.5f }); mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.25f); - ensureFloats("float A again", "env_intensity", { 0.25f }); + require_floats("float A again", "env_intensity", { 0.25f }); const GLint ivec_a[4] = { 1, 2, 3, 4 }; const GLint ivec_b[4] = { 1, 2, 3, 5 }; mShader.uniform4iv(LLShaderMgr::SUN_UP_FACTOR, 1, ivec_a); mShader.uniform4iv(LLShaderMgr::SUN_UP_FACTOR, 1, ivec_b); mShader.uniform4iv(LLShaderMgr::SUN_UP_FACTOR, 1, ivec_a); - ensureInts("ivec4 A again", "sun_up_factor", { 1, 2, 3, 4 }); + require_ints("ivec4 A again", "sun_up_factor", { 1, 2, 3, 4 }); mShader.uniform1i(LLShaderMgr::MULTI_LIGHT_COUNT, 3); const GLint count = 4; mShader.uniform1iv(LLShaderMgr::MULTI_LIGHT_COUNT, 1, &count); - ensureInts("int B", "light_count", { 4 }); + require_ints("int B", "light_count", { 4 }); mShader.uniform1i(LLShaderMgr::MULTI_LIGHT_COUNT, 3); - ensureInts("int A again", "light_count", { 3 }); + require_ints("int A again", "light_count", { 3 }); mShader.uniform1ui(LLShaderMgr::FRAME_ID, 7u); mShader.uniform1ui(LLShaderMgr::FRAME_ID, 8u); mShader.uniform1ui(LLShaderMgr::FRAME_ID, 7u); - ensureUints("uint A again", "uFrameId", { 7u }); + require_uints("uint A again", "uFrameId", { 7u }); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -897,20 +894,19 @@ namespace glUniform1f(loc, 5.f); mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.f); - ensureFloats("the first set uploads, a zero too", "env_intensity", { 0.f }); + require_floats("the first set uploads, a zero too", "env_intensity", { 0.f }); mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.5f); mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.5f); - ensureFloats("set twice", "env_intensity", { 0.5f }); + require_floats("set twice", "env_intensity", { 0.5f }); glUniform1f(loc, 2.f); mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.5f); - ensureFloats("the value last uploaded is not uploaded again", "env_intensity", { 2.f }); + require_floats("the value last uploaded is not uploaded again", "env_intensity", { 2.f }); mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.75f); - ensureFloats("a new value replaces it", "env_intensity", { 0.75f }); + require_floats("a new value replaces it", "env_intensity", { 0.75f }); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -921,20 +917,18 @@ namespace setUp(); const GLint before = location("color"); mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 1.f); - ensureFloats("set before the relink", "color", { 0.25f, 0.5f, 0.75f, 1.f }); + require_floats("set before the relink", "color", { 0.25f, 0.5f, 0.75f, 1.f }); - UNSCOPED_INFO("program relinks"); CHECK(mShader.link()); if (location("color") != before) { SKIP("the relink moved the uniform, so a stale slot would not be consulted"); } - ensureFloats("the relink reset it", "color", { 0.f, 0.f, 0.f, 0.f }); + require_floats("the relink reset it", "color", { 0.f, 0.f, 0.f, 0.f }); mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 1.f); - ensureFloats("set again after the relink", "color", { 0.25f, 0.5f, 0.75f, 1.f }); + require_floats("set again after the relink", "color", { 0.25f, 0.5f, 0.75f, 1.f }); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -946,32 +940,30 @@ namespace const GLint before = location("color"); mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 1.f); mShader.setMinimumAlpha(0.5f); - ensureFloats("colour on the old program", "color", { 0.25f, 0.5f, 0.75f, 1.f }); - ensureFloats("minimum alpha on the old program", "minimum_alpha", { 0.5f }); + require_floats("colour on the old program", "color", { 0.25f, 0.5f, 0.75f, 1.f }); + require_floats("minimum alpha on the old program", "minimum_alpha", { 0.5f }); const GLuint fresh = linkRaw(); UNSCOPED_INFO("the new program links"); - CHECK(fresh != 0); + REQUIRE(fresh != 0); LLGLSLShader::unbind(); const GLuint old = mShader.mProgramObject; mShader.mProgramObject = fresh; forget_program_samplers(old); glDeleteProgram(old); - UNSCOPED_INFO("the new program maps"); CHECK(mShader.mapUniforms()); mShader.bind(); if (location("color") != before) { SKIP("the new program put the uniform elsewhere, so a stale slot would not be consulted"); } - ensureFloats("the new program starts at 0", "color", { 0.f, 0.f, 0.f, 0.f }); + require_floats("the new program starts at 0", "color", { 0.f, 0.f, 0.f, 0.f }); mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 1.f); - ensureFloats("colour set again", "color", { 0.25f, 0.5f, 0.75f, 1.f }); + require_floats("colour set again", "color", { 0.25f, 0.5f, 0.75f, 1.f }); mShader.setMinimumAlpha(0.5f); - ensureFloats("minimum alpha set again", "minimum_alpha", { 0.5f }); + require_floats("minimum alpha set again", "minimum_alpha", { 0.5f }); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -983,21 +975,20 @@ namespace setUp(); mShader.uniform1i(LLShaderMgr::GRAIN_ANIMATE, 1); - ensureInts("true by int", "uGrainAnimate", { 1 }); + require_ints("true by int", "uGrainAnimate", { 1 }); mShader.uniform1f(LLShaderMgr::GRAIN_ANIMATE, 0.f); - ensureInts("false by float", "uGrainAnimate", { 0 }); + require_ints("false by float", "uGrainAnimate", { 0 }); mShader.uniform1i(LLShaderMgr::GRAIN_ANIMATE, 1); - ensureInts("true by int again", "uGrainAnimate", { 1 }); + require_ints("true by int again", "uGrainAnimate", { 1 }); // Read at run time, so nothing folds the sign of the zero away. volatile U32 sign_bit = 0x80000000u; const F32 negative_zero = std::bit_cast(static_cast(sign_bit)); mShader.uniform1i(LLShaderMgr::GRAIN_ANIMATE, INT_MIN); - ensureInts("true by INT_MIN", "uGrainAnimate", { 1 }); + require_ints("true by INT_MIN", "uGrainAnimate", { 1 }); mShader.uniform1f(LLShaderMgr::GRAIN_ANIMATE, negative_zero); - ensureInts("false by -0.0, the same bits", "uGrainAnimate", { 0 }); + require_ints("false by -0.0, the same bits", "uGrainAnimate", { 0 }); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -1009,13 +1000,12 @@ namespace mShader.uniform1i(LLShaderMgr::MULTI_LIGHT_COUNT, 3); mShader.uniform1i(light_count, 4); - ensureInts("by name", "light_count", { 4 }); + require_ints("by name", "light_count", { 4 }); mShader.uniform1i(LLShaderMgr::MULTI_LIGHT_COUNT, 3); - ensureInts("by index after the name", "light_count", { 3 }); + require_ints("by index after the name", "light_count", { 3 }); mShader.uniform1i(light_count, 4); - ensureInts("by name after the index", "light_count", { 4 }); + require_ints("by name after the index", "light_count", { 4 }); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -1027,19 +1017,18 @@ namespace mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.5f); mShader.fastUniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.25f); - ensureFloats("fast float", "env_intensity", { 0.25f }); + require_floats("fast float", "env_intensity", { 0.25f }); mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.5f); - ensureFloats("cached float after the fast one", "env_intensity", { 0.5f }); + require_floats("cached float after the fast one", "env_intensity", { 0.5f }); const F32 a[4] = { 0.25f, 0.5f, 0.75f, 1.f }; const F32 b[4] = { 1.f, 0.75f, 0.5f, 0.25f }; mShader.uniform4fv(LLShaderMgr::SPECULAR_COLOR, 1, a); mShader.fastUniform4fv(LLShaderMgr::SPECULAR_COLOR, 1, b); - ensureFloats("fast vec4", "specular_color", { 1.f, 0.75f, 0.5f, 0.25f }); + require_floats("fast vec4", "specular_color", { 1.f, 0.75f, 0.5f, 0.25f }); mShader.uniform4fv(LLShaderMgr::SPECULAR_COLOR, 1, a); - ensureFloats("cached vec4 after the fast one", "specular_color", { 0.25f, 0.5f, 0.75f, 1.f }); + require_floats("cached vec4 after the fast one", "specular_color", { 0.25f, 0.5f, 0.75f, 1.f }); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -1055,19 +1044,18 @@ namespace const F32 two_again[8] = { 1.f, 1.f, 1.f, 1.f, 0.25f, 0.25f, 0.25f, 0.25f }; mShader.uniform4fv(LLShaderMgr::LIGHT_DIFFUSE, 1, single); - ensureFloats("element 0 alone", "light_diffuse[0]", { 0.25f, 0.5f, 0.75f, 1.f }); + require_floats("element 0 alone", "light_diffuse[0]", { 0.25f, 0.5f, 0.75f, 1.f }); mShader.uniform4fv(LLShaderMgr::LIGHT_DIFFUSE, 2, two); - ensureFloats("element 0 by the array", "light_diffuse[0]", { 1.f, 1.f, 1.f, 1.f }); - ensureFloats("element 1 by the array", "light_diffuse[1]", { 0.5f, 0.5f, 0.5f, 0.5f }); + require_floats("element 0 by the array", "light_diffuse[0]", { 1.f, 1.f, 1.f, 1.f }); + require_floats("element 1 by the array", "light_diffuse[1]", { 0.5f, 0.5f, 0.5f, 0.5f }); mShader.uniform4fv(LLShaderMgr::LIGHT_DIFFUSE, 1, single); - ensureFloats("element 0 alone again", "light_diffuse[0]", { 0.25f, 0.5f, 0.75f, 1.f }); + require_floats("element 0 alone again", "light_diffuse[0]", { 0.25f, 0.5f, 0.75f, 1.f }); mShader.uniform4fv(LLShaderMgr::LIGHT_DIFFUSE, 2, two); mShader.uniform4fv(LLShaderMgr::LIGHT_DIFFUSE, 2, two_again); - ensureFloats("element 1 by an array whose element 0 did not change", "light_diffuse[1]", - { 0.25f, 0.25f, 0.25f, 0.25f }); + require_floats("element 1 by an array whose element 0 did not change", "light_diffuse[1]", + { 0.25f, 0.25f, 0.25f, 0.25f }); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -1081,26 +1069,17 @@ namespace } for (U32 i = 0; i < 3; ++i) { - UNSCOPED_INFO("work group count " + std::to_string(i)); + CAPTURE(i); REQUIRE(gGLManager.mMaxComputeWorkGroupCount[i] >= 65535); } - UNSCOPED_INFO("work group size x"); CHECK(gGLManager.mMaxComputeWorkGroupSize[0] >= 1024); - UNSCOPED_INFO("work group size y"); CHECK(gGLManager.mMaxComputeWorkGroupSize[1] >= 1024); - UNSCOPED_INFO("work group size z"); CHECK(gGLManager.mMaxComputeWorkGroupSize[2] >= 64); - UNSCOPED_INFO("invocations"); CHECK(gGLManager.mMaxComputeWorkGroupInvocations >= 1024); - UNSCOPED_INFO("shared memory"); CHECK(gGLManager.mMaxComputeSharedMemorySize >= 32768); - UNSCOPED_INFO("image units"); CHECK(gGLManager.mMaxImageUnits >= 8); - UNSCOPED_INFO("compute storage blocks"); CHECK(gGLManager.mMaxComputeShaderStorageBlocks >= 8); - UNSCOPED_INFO("storage bindings"); CHECK(gGLManager.mMaxShaderStorageBufferBindings >= 8); - UNSCOPED_INFO("storage block size"); CHECK(gGLManager.mMaxShaderStorageBlockSize >= (S64(1) << 27)); const S32 alignment = gGLManager.mShaderStorageBufferOffsetAlignment; { @@ -1108,7 +1087,6 @@ namespace CHECK(alignment > 0); CHECK((alignment & (alignment - 1)) == 0); } - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -1119,32 +1097,24 @@ namespace TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: createShader builds a compute program that reaches each index once", "[llglslshader_gl]") { setUpCompute(); - UNSCOPED_INFO("a compute stage at GLSL 4.30"); CHECK(compiledVersion(mShaders, "test/countC.glsl", GL_COMPUTE_SHADER, 0) == std::string("#version 430")); - UNSCOPED_INFO("count program builds"); - CHECK(buildProgram(mCompute, "count (test)", { { "test/countC.glsl", GL_COMPUTE_SHADER } })); - UNSCOPED_INFO("it is compute"); + REQUIRE(buildProgram(mCompute, "count (test)", { { "test/countC.glsl", GL_COMPUTE_SHADER } })); CHECK(mCompute.isCompute()); UNSCOPED_INFO("it holds no object once linked"); - REQUIRE(attachedNames(mCompute.mProgramObject).empty()); + CHECK(attachedNames(mCompute.mProgramObject).empty()); - UNSCOPED_INFO("local size x"); CHECK(mCompute.mComputeLocalSize[0] == 64u); - UNSCOPED_INFO("local size y"); CHECK(mCompute.mComputeLocalSize[1] == 1u); - UNSCOPED_INFO("local size z"); CHECK(mCompute.mComputeLocalSize[2] == 1u); UNSCOPED_INFO("the table gives its block a point"); CHECK(mShaders.getStorageBlockBinding(TEST_COUNTS) >= 0); UNSCOPED_INFO("its block is at the table's point"); - CHECK(mCompute.getStorageBinding(TEST_COUNTS) == - mShaders.getStorageBlockBinding(TEST_COUNTS)); + CHECK(mCompute.getStorageBinding(TEST_COUNTS) == mShaders.getStorageBlockBinding(TEST_COUNTS)); // 15.6 groups' worth, in a buffer with room for 17. constexpr U32 COUNT = 1000; constexpr U32 WORDS = 17 * 64; - UNSCOPED_INFO("counts allocate"); - CHECK(mBuffers[0].allocate(WORDS * sizeof(U32), 0, nullptr, "counts (test)")); + REQUIRE(mBuffers[0].allocate(WORDS * sizeof(U32), 0, nullptr, "counts (test)")); mBuffers[0].clear(); mCompute.bind(); @@ -1155,11 +1125,9 @@ namespace const std::vector hits = readWords(mBuffers[0], WORDS); for (U32 i = 0; i < WORDS; ++i) { - UNSCOPED_INFO("index " + std::to_string(i) + (i < COUNT ? " once" : " untouched")); - REQUIRE(hits[i] == - (i < COUNT ? 1u : 0u)); + CAPTURE(i); + REQUIRE(hits[i] == (i < COUNT ? 1u : 0u)); } - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -1176,29 +1144,24 @@ namespace } const std::string usual = gGLManager.mGLSLVersionMinor >= 20 ? "#version 420" : "#version 400"; - UNSCOPED_INFO("ordinary program builds"); CHECK(buildProgram(mDraw, "ladder (test)", - { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/solidF.glsl", GL_FRAGMENT_SHADER } })); - UNSCOPED_INFO("an ordinary stage at the usual version"); + { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/solidF.glsl", GL_FRAGMENT_SHADER } })); CHECK(compiledVersion(mShaders, "test/solidF.glsl", GL_FRAGMENT_SHADER, 0) == usual); size_t shared = 0; for (const auto& stage : mShaders.mShaderObjects) { for (const auto& object : stage) { - UNSCOPED_INFO(object.first + " at the usual version"); + CAPTURE(object.first); REQUIRE(firstSourceLine(object.second) == usual); ++shared; } } - UNSCOPED_INFO("the three shared objects"); CHECK(shared == size_t(3)); if (ll_test::contextHasCompute()) { - UNSCOPED_INFO("a stage asking for 4.30 at 4.30"); - CHECK(compiledVersion(mShaders, "test/solidF.glsl", GL_FRAGMENT_SHADER, 430) == - std::string("#version 430")); + CHECK(compiledVersion(mShaders, "test/solidF.glsl", GL_FRAGMENT_SHADER, 430) == std::string("#version 430")); if (gGLManager.mMaxVertexShaderStorageBlocks >= 1) { const std::initializer_list> reader = @@ -1230,7 +1193,6 @@ namespace CHECK_FALSE(compute_built); UNSCOPED_INFO("no program asking for 4.30 below it"); CHECK_FALSE(raised_built); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -1241,17 +1203,13 @@ namespace setUpCompute(); constexpr U32 W = 37; constexpr U32 H = 23; - UNSCOPED_INFO("R32F target allocates"); - CHECK(mTargets[0].allocate(W, H, GL_R32F)); - UNSCOPED_INFO("RGBA16F target allocates"); - CHECK(mTargets[1].allocate(W, H, GL_RGBA16F)); + REQUIRE(mTargets[0].allocate(W, H, GL_R32F)); + REQUIRE(mTargets[1].allocate(W, H, GL_RGBA16F)); UNSCOPED_INFO("output target allocates"); - CHECK(mTargets[2].allocate(W, H, GL_RGBA32F)); - UNSCOPED_INFO("store program builds"); - CHECK(buildProgram(mCompute, "image store (test)", { { "test/imageStoreC.glsl", GL_COMPUTE_SHADER } })); - UNSCOPED_INFO("sample program builds"); - CHECK(buildProgram(mDraw, "image sample (test)", - { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/sampleF.glsl", GL_FRAGMENT_SHADER } })); + REQUIRE(mTargets[2].allocate(W, H, GL_RGBA32F)); + REQUIRE(buildProgram(mCompute, "image store (test)", { { "test/imageStoreC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mDraw, "image sample (test)", + { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/sampleF.glsl", GL_FRAGMENT_SHADER } })); const S32 r32f_unit = mCompute.getImageUnit(TEST_IMAGE_R32F); const S32 rgba_unit = mCompute.getImageUnit(TEST_IMAGE_RGBA16F); @@ -1259,7 +1217,6 @@ namespace CHECK(r32f_unit >= 0); UNSCOPED_INFO("RGBA16F image has a unit"); CHECK(rgba_unit >= 0); - UNSCOPED_INFO("the units differ"); CHECK(r32f_unit != rgba_unit); mCompute.bind(); @@ -1278,17 +1235,12 @@ namespace { for (U32 x = 0; x < W; ++x) { - const std::string at = " at " + std::to_string(x) + "," + std::to_string(y); + CAPTURE(x, y); const size_t i = (size_t)y * W + x; - UNSCOPED_INFO("R32F" + at); REQUIRE(r32f[i] == F32(x * 1000 + y)); - UNSCOPED_INFO("RGBA16F r" + at); REQUIRE(rgba[i * 4 + 0] == F32(x) * 0.125f); - UNSCOPED_INFO("RGBA16F g" + at); REQUIRE(rgba[i * 4 + 1] == F32(y) * 0.125f); - UNSCOPED_INFO("RGBA16F b" + at); REQUIRE(rgba[i * 4 + 2] == 0.25f); - UNSCOPED_INFO("RGBA16F a" + at); REQUIRE(rgba[i * 4 + 3] == 1.f); } } @@ -1296,11 +1248,11 @@ namespace LLGLDisable blend(GL_BLEND); for (U32 source = 0; source < 2; ++source) { + INFO("sampling the " << (source ? "RGBA16F" : "R32F") << " target"); mTargets[2].bindTarget(); gGL.setClearColor(LLColor4::transparent); mTargets[2].clear(); mDraw.bind(); - UNSCOPED_INFO("source bound"); REQUIRE(mDraw.bindTexture(TEST_SOURCE_MAP, &mTargets[source], ALSamplers::PointClamp) >= 0); gGL.drawArraysInstanced(LLRender::TRIANGLES, 0, 3, 1); const std::vector drawn = readBoundTarget(W, H, 4, GL_RGBA); @@ -1311,8 +1263,7 @@ namespace { for (U32 x = 0; x < W; ++x) { - const std::string at = (source ? " RGBA16F" : " R32F") + std::string(" sampled at ") - + std::to_string(x) + "," + std::to_string(y); + CAPTURE(x, y); const size_t i = ((size_t)y * W + x) * 4; const F32 expected[4] = { @@ -1323,13 +1274,12 @@ namespace }; for (U32 c = 0; c < 4; ++c) { - UNSCOPED_INFO("component " + std::to_string(c) + at); + CAPTURE(c); REQUIRE(drawn[i + c] == expected[c]); } } } } - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -1351,16 +1301,13 @@ namespace // One vertex a point, no instances yet, then room for every id. std::vector list(4 + COUNT, 0u); list[0] = 1u; - UNSCOPED_INFO("list allocates"); REQUIRE(mBuffers[0].allocate(list.size() * sizeof(U32), 0, list.data(), "compaction (test)")); UNSCOPED_INFO("output target allocates"); - CHECK(mTargets[0].allocate(SIDE, SIDE, GL_R32F)); - UNSCOPED_INFO("compaction builds"); - CHECK(buildProgram(mCompute, "compaction (test)", { { "test/compactC.glsl", GL_COMPUTE_SHADER } })); - UNSCOPED_INFO("draw builds"); - CHECK(buildProgram(mDraw, "compaction draw (test)", - { { "test/compactV.glsl", GL_VERTEX_SHADER }, { "test/compactF.glsl", GL_FRAGMENT_SHADER } }, - 430)); + REQUIRE(mTargets[0].allocate(SIDE, SIDE, GL_R32F)); + REQUIRE(buildProgram(mCompute, "compaction (test)", { { "test/compactC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mDraw, "compaction draw (test)", + { { "test/compactV.glsl", GL_VERTEX_SHADER }, { "test/compactF.glsl", GL_FRAGMENT_SHADER } }, + 430)); const S32 point = mShaders.getStorageBlockBinding(TEST_DRAW_LIST); UNSCOPED_INFO("the kernel's list at the table's point"); CHECK(mCompute.getStorageBinding(TEST_DRAW_LIST) == point); @@ -1388,13 +1335,11 @@ namespace for (U32 id = 0; id < SIDE * SIDE; ++id) { const bool kept = id < COUNT && id % 3 == 0; - UNSCOPED_INFO("id " + std::to_string(id) + (kept ? " drawn" : " not drawn")); - REQUIRE(drawn[id] == - (kept ? F32(id + 1) : 0.f)); + CAPTURE(id, kept); + REQUIRE(drawn[id] == (kept ? F32(id + 1) : 0.f)); } UNSCOPED_INFO("instances counted"); CHECK(readWords(mBuffers[0], 2)[1] == KEPT); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -1404,14 +1349,10 @@ namespace { setUpCompute(); const std::vector zeros(8, 0u); - UNSCOPED_INFO("command buffer allocates"); - CHECK(mBuffers[0].allocate(8 * sizeof(U32), 0, zeros.data(), "dispatch (test)")); - UNSCOPED_INFO("counts allocate"); - CHECK(mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "counts (test)")); - UNSCOPED_INFO("writer builds"); - CHECK(buildProgram(mCompute, "dispatch writer (test)", { { "test/indirectWriteC.glsl", GL_COMPUTE_SHADER } })); - UNSCOPED_INFO("counter builds"); - CHECK(buildProgram(mCompute2, "dispatch counter (test)", { { "test/indirectCountC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(mBuffers[0].allocate(8 * sizeof(U32), 0, zeros.data(), "dispatch (test)")); + REQUIRE(mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "counts (test)")); + REQUIRE(buildProgram(mCompute, "dispatch writer (test)", { { "test/indirectWriteC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mCompute2, "dispatch counter (test)", { { "test/indirectCountC.glsl", GL_COMPUTE_SHADER } })); mCompute.bind(); mCompute.uniform1i(TEST_COUNT, 5); @@ -1439,7 +1380,6 @@ namespace CHECK(hits[2] == 2u); UNSCOPED_INFO("groups in z"); CHECK(hits[3] == 1u); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -1448,8 +1388,7 @@ namespace TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: storage blocks declared out of the table's order still take its points", "[llglslshader_gl]") { setUpCompute(); - UNSCOPED_INFO("remap builds"); - CHECK(buildProgram(mCompute, "block remap (test)", { { "test/remapC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mCompute, "block remap (test)", { { "test/remapC.glsl", GL_COMPUTE_SHADER } })); const S32 first = mShaders.getStorageBlockBinding(TEST_BLOCK_FIRST); UNSCOPED_INFO("the first block at its point"); CHECK(mCompute.getStorageBinding(TEST_BLOCK_FIRST) == first); @@ -1461,10 +1400,8 @@ namespace CHECK(mCompute.getStorageBinding(TEST_STORAGE_END) == -1); const std::vector zeros(4, 0u); - UNSCOPED_INFO("a allocates"); - CHECK(mBuffers[0].allocate(4 * sizeof(U32), 0, zeros.data(), "a (test)")); - UNSCOPED_INFO("b allocates"); - CHECK(mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "b (test)")); + REQUIRE(mBuffers[0].allocate(4 * sizeof(U32), 0, zeros.data(), "a (test)")); + REQUIRE(mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "b (test)")); mCompute.bind(); mCompute.bindStorageBuffer(TEST_BLOCK_FIRST, mBuffers[0]); @@ -1485,7 +1422,6 @@ namespace CHECK(readWords(mBuffers[0], 1)[0] == 222u); UNSCOPED_INFO("swapped, b holds the first block"); CHECK(readWords(mBuffers[1], 1)[0] == 111u); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -1495,8 +1431,7 @@ namespace { setUpCompute(); constexpr U32 SIDE = 16; - UNSCOPED_INFO("units builds"); - CHECK(buildProgram(mCompute, "image units (test)", { { "test/imageUnitsC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mCompute, "image units (test)", { { "test/imageUnitsC.glsl", GL_COMPUTE_SHADER } })); UNSCOPED_INFO("the first reserved image first"); CHECK(mCompute.getImageUnit(TEST_IMAGE_A) == 0); UNSCOPED_INFO("the second reserved image second"); @@ -1508,14 +1443,10 @@ namespace UNSCOPED_INFO("nor a texture unit"); CHECK(mCompute.getTextureChannel(TEST_IMAGE_A) == -1); - UNSCOPED_INFO("a allocates"); - CHECK(mTargets[0].allocate(SIDE, SIDE, GL_R32F)); - UNSCOPED_INFO("b allocates"); - CHECK(mTargets[1].allocate(SIDE, SIDE, GL_R32F)); + REQUIRE(mTargets[0].allocate(SIDE, SIDE, GL_R32F)); + REQUIRE(mTargets[1].allocate(SIDE, SIDE, GL_R32F)); mCompute.bind(); - UNSCOPED_INFO("a bound"); CHECK(mCompute.bindImage(TEST_IMAGE_A, &mTargets[0], ALImageSlot::WRITE_ONLY) == 0); - UNSCOPED_INFO("b bound"); CHECK(mCompute.bindImage(TEST_IMAGE_B, &mTargets[1], ALImageSlot::WRITE_ONLY) == 1); mCompute.dispatchThreads(SIDE, SIDE); gGL.memoryBarrier(LLRender::BARRIER_TEXTURE_UPDATE); @@ -1526,12 +1457,12 @@ namespace const std::vector b = readTexture(mTargets[1].getTexture(), SIDE, SIDE, 1, GL_RED, GL_FLOAT); for (U32 i = 0; i < SIDE * SIDE; ++i) { - UNSCOPED_INFO("a holds image A at " + std::to_string(i)); + CAPTURE(i); + UNSCOPED_INFO("a holds image A"); REQUIRE(a[i] == 1.f); - UNSCOPED_INFO("b holds image B at " + std::to_string(i)); + UNSCOPED_INFO("b holds image B"); REQUIRE(b[i] == 2.f); } - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -1542,13 +1473,10 @@ namespace setUpCompute(); constexpr U32 W = 13; constexpr U32 H = 7; - UNSCOPED_INFO("sample builds"); - CHECK(buildProgram(mCompute, "uint sample (test)", { { "test/uintSampleC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mCompute, "uint sample (test)", { { "test/uintSampleC.glsl", GL_COMPUTE_SHADER } })); const S32 float_unit = mCompute.getTextureChannel(LLShaderMgr::DIFFUSE_MAP); const S32 uint_unit = mCompute.getTextureChannel(TEST_UINT_MAP); - UNSCOPED_INFO("the float sampler first"); CHECK(float_unit == 0); - UNSCOPED_INFO("the unsigned one after it"); CHECK(uint_unit == 1); std::vector values(W * H); @@ -1558,20 +1486,14 @@ namespace } mUintImage = new LLImageGL(W, H, 1, /*usemipmaps=*/false); mUintImage->setExplicitFormat(GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT); - UNSCOPED_INFO("R32UI image created"); - CHECK(mUintImage->createGLTexture(0, reinterpret_cast(values.data()))); - UNSCOPED_INFO("RGBA8 target allocates"); - CHECK(mTargets[1].allocate(W, H, GL_RGBA8)); - UNSCOPED_INFO("out allocates"); - CHECK(mBuffers[0].allocate(W * H * sizeof(U32), 0, nullptr, "uint sample (test)")); + REQUIRE(mUintImage->createGLTexture(0, reinterpret_cast(values.data()))); + REQUIRE(mTargets[1].allocate(W, H, GL_RGBA8)); + REQUIRE(mBuffers[0].allocate(W * H * sizeof(U32), 0, nullptr, "uint sample (test)")); mBuffers[0].clear(0xFFFFFFFFu); mCompute.bind(); mCompute.uniform1i(TEST_COUNT, 1); - UNSCOPED_INFO("float texture bound"); - CHECK(mCompute.bindTexture(LLShaderMgr::DIFFUSE_MAP, &mTargets[1], ALSamplers::PointClamp) == - float_unit); - UNSCOPED_INFO("unsigned texture bound"); + CHECK(mCompute.bindTexture(LLShaderMgr::DIFFUSE_MAP, &mTargets[1], ALSamplers::PointClamp) == float_unit); CHECK(gGL.getTextureSlot((U32)uint_unit)->bindSampled(mUintImage.get(), ALSamplers::PointClamp)); mCompute.bindStorageBuffer(TEST_COUNTS, mBuffers[0]); mCompute.dispatchThreads(W, H); @@ -1581,10 +1503,9 @@ namespace const std::vector read = readWords(mBuffers[0], W * H); for (U32 i = 0; i < W * H; ++i) { - UNSCOPED_INFO("texel " + std::to_string(i)); + CAPTURE(i); REQUIRE(read[i] == values[i]); } - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -1597,23 +1518,18 @@ namespace setUpShaders(); constexpr U32 W = 11; constexpr U32 H = 6; - UNSCOPED_INFO("R32UI target allocates"); - CHECK(mTargets[0].allocate(W, H, GL_R32UI)); - UNSCOPED_INFO("RG16I target allocates"); - CHECK(mTargets[1].allocate(W, H, GL_RG16I)); + REQUIRE(mTargets[0].allocate(W, H, GL_R32UI)); + REQUIRE(mTargets[1].allocate(W, H, GL_RG16I)); UNSCOPED_INFO("output target allocates"); - CHECK(mTargets[2].allocate(W, H, GL_RGBA32UI)); - UNSCOPED_INFO("its signed attachment"); - CHECK(mTargets[2].addColorAttachment(GL_RGBA32I)); - UNSCOPED_INFO("unsigned writer builds"); - CHECK(buildProgram(mDraw, "uint write (test)", - { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/uintWriteF.glsl", GL_FRAGMENT_SHADER } })); - UNSCOPED_INFO("signed writer builds"); - CHECK(buildProgram(mDraw2, "int write (test)", - { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/intWriteF.glsl", GL_FRAGMENT_SHADER } })); - UNSCOPED_INFO("reader builds"); - CHECK(buildProgram(mDraw3, "integer sample (test)", - { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/intSampleF.glsl", GL_FRAGMENT_SHADER } })); + REQUIRE(mTargets[2].allocate(W, H, GL_RGBA32UI)); + UNSCOPED_INFO("the output's signed attachment"); + REQUIRE(mTargets[2].addColorAttachment(GL_RGBA32I)); + REQUIRE(buildProgram(mDraw, "uint write (test)", + { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/uintWriteF.glsl", GL_FRAGMENT_SHADER } })); + REQUIRE(buildProgram(mDraw2, "int write (test)", + { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/intWriteF.glsl", GL_FRAGMENT_SHADER } })); + REQUIRE(buildProgram(mDraw3, "integer sample (test)", + { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/intSampleF.glsl", GL_FRAGMENT_SHADER } })); UNSCOPED_INFO("the unsigned sampler has a unit"); CHECK(mDraw3.getTextureChannel(TEST_UINT_MAP) >= 0); UNSCOPED_INFO("the signed sampler has a unit"); @@ -1630,14 +1546,13 @@ namespace mTargets[2].bindTarget(); mDraw3.bind(); - UNSCOPED_INFO("unsigned source bound"); CHECK(mDraw3.bindTexture(TEST_UINT_MAP, &mTargets[0]) >= 0); - UNSCOPED_INFO("signed source bound"); CHECK(mDraw3.bindTexture(TEST_INT_MAP, &mTargets[1]) >= 0); // Read off GL, since a driver may read an integer texture under a filtering sampler texel by texel anyway. for (const S32 uniform : { (S32)TEST_UINT_MAP, (S32)TEST_INT_MAP }) { + INFO((uniform == TEST_UINT_MAP ? "the unsigned source's sampler" : "the signed source's sampler")); glActiveTexture(GL_TEXTURE0 + mDraw3.getTextureChannel(uniform)); GLint sampler = 0; glGetIntegerv(GL_SAMPLER_BINDING, &sampler); @@ -1645,12 +1560,8 @@ namespace GLint mag_filter = 0; glGetSamplerParameteriv((GLuint)sampler, GL_TEXTURE_MIN_FILTER, &min_filter); glGetSamplerParameteriv((GLuint)sampler, GL_TEXTURE_MAG_FILTER, &mag_filter); - const std::string which = uniform == TEST_UINT_MAP ? "the unsigned source's" : "the signed source's"; - UNSCOPED_INFO(which + " sampler magnifies by the nearest texel"); REQUIRE((U32)mag_filter == (U32)GL_NEAREST); - UNSCOPED_INFO(which + " sampler minifies by the nearest texel"); - REQUIRE((U32)min_filter == - (U32)GL_NEAREST_MIPMAP_NEAREST); + REQUIRE((U32)min_filter == (U32)GL_NEAREST_MIPMAP_NEAREST); } glActiveTexture(GL_TEXTURE0 + gGL.getCurrentTexUnitIndex()); // And the debug validator, which checks every sampler a draw reads, finds nothing to say -- where a filtering @@ -1681,20 +1592,18 @@ namespace { for (U32 x = 0; x < W; ++x) { - const std::string at = " at " + std::to_string(x) + "," + std::to_string(y); + CAPTURE(x, y); const size_t i = ((size_t)y * W + x) * 4; const U32 uint_expected[4] = { 0x01000001u + y * 1000u + x, 0u, 0u, 1u }; const S32 int_expected[4] = { -1 - (S32)x, (S32)y * 300 - 1000, 0, 1 }; for (U32 c = 0; c < 4; ++c) { - UNSCOPED_INFO("unsigned component " + std::to_string(c) + at); + CAPTURE(c); REQUIRE(uints[i + c] == uint_expected[c]); - UNSCOPED_INFO("signed component " + std::to_string(c) + at); REQUIRE(ints[i + c] == int_expected[c]); } } } - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -1710,7 +1619,7 @@ namespace setUpShaders(); constexpr U32 SIDE = 4; UNSCOPED_INFO("output target allocates"); - CHECK(mTargets[0].allocate(SIDE, SIDE, GL_RGBA32F)); + REQUIRE(mTargets[0].allocate(SIDE, SIDE, GL_RGBA32F)); const auto& fragments = mShaders.mShaderObjects[LLShaderMgr::STAGE_FRAGMENT]; const auto found = fragments.find("deferred/globalF.glsl"); @@ -1725,10 +1634,11 @@ namespace }; const U32 live = LLShaderMgr::sLiveObjects; - // What `program` draws, every pixel the colour the stub passes through. + // That `program` draws every pixel the colour the stub passes through. LLGLDisable blend(GL_BLEND); - auto ensureDraws = [&](const std::string& what, LLGLSLShader& program) + auto require_draws = [&](const std::string& what, LLGLSLShader& program) { + INFO(what); mTargets[0].bindTarget(); gGL.setClearColor(LLColor4::transparent); mTargets[0].clear(); @@ -1740,19 +1650,17 @@ namespace const F32 expected[4] = { 0.25f, 0.5f, 0.75f, 1.f }; for (size_t i = 0; i < drawn.size(); ++i) { - UNSCOPED_INFO(what + ", component " + std::to_string(i)); + CAPTURE(i); REQUIRE(drawn[i] == expected[i % 4]); } }; - UNSCOPED_INFO("A builds"); - CHECK(buildProgram(mDraw, "shared A (test)", files)); - UNSCOPED_INFO("B builds"); - CHECK(buildProgram(mDraw2, "shared B (test)", files)); + REQUIRE(buildProgram(mDraw, "shared A (test)", files)); + REQUIRE(buildProgram(mDraw2, "shared B (test)", files)); UNSCOPED_INFO("A holds no object once linked"); - REQUIRE(attachedNames(mDraw.mProgramObject).empty()); + CHECK(attachedNames(mDraw.mProgramObject).empty()); UNSCOPED_INFO("nor does B"); - REQUIRE(attachedNames(mDraw2.mProgramObject).empty()); + CHECK(attachedNames(mDraw2.mProgramObject).empty()); UNSCOPED_INFO("their own objects deleted"); CHECK(LLShaderMgr::sLiveObjects == live); UNSCOPED_INFO("a program's own stage is not shared"); @@ -1761,7 +1669,7 @@ namespace CHECK(glIsShader(shared) == GL_TRUE); UNSCOPED_INFO("not even flagged for deletion"); CHECK_FALSE(flaggedForDeletion(shared)); - ensureDraws("A draws", mDraw); + require_draws("A draws", mDraw); mDraw.unload(); UNSCOPED_INFO("unloading A leaves the shared object"); @@ -1769,16 +1677,15 @@ namespace glValidateProgram(mDraw2.mProgramObject); GLint valid = GL_FALSE; glGetProgramiv(mDraw2.mProgramObject, GL_VALIDATE_STATUS, &valid); - UNSCOPED_INFO("B validates"); CHECK(valid == GL_TRUE); - ensureDraws("B draws", mDraw2); + require_draws("B draws", mDraw2); mDraw2.unload(); - UNSCOPED_INFO("C builds afterwards"); - CHECK(buildProgram(mDraw3, "shared C (test)", files)); + UNSCOPED_INFO("C builds after A and B are gone"); + REQUIRE(buildProgram(mDraw3, "shared C (test)", files)); UNSCOPED_INFO("C holds no object once linked"); - REQUIRE(attachedNames(mDraw3.mProgramObject).empty()); - ensureDraws("C draws", mDraw3); + CHECK(attachedNames(mDraw3.mProgramObject).empty()); + require_draws("C draws", mDraw3); // Without the shared object to call into, the stage cannot link, at any class. UNSCOPED_INFO("D does not link"); @@ -1790,7 +1697,7 @@ namespace const GLuint loose = mShaders.loadShaderFile("test/solidF.glsl", level, GL_FRAGMENT_SHADER); UNSCOPED_INFO("an object no program holds"); CHECK((loose != 0 && glIsShader(loose) == GL_TRUE)); - UNSCOPED_INFO("counted live"); + UNSCOPED_INFO("the loose object counted live"); CHECK(LLShaderMgr::sLiveObjects == live + 1); size_t held = 0; @@ -1810,8 +1717,7 @@ namespace CHECK(glIsShader(shared) == GL_FALSE); UNSCOPED_INFO("none counted live"); CHECK(LLShaderMgr::sLiveObjects == live + 1 - (U32)held); - ensureDraws("C still draws", mDraw3); - UNSCOPED_INFO("no GL error"); + require_draws("C still draws", mDraw3); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -1823,10 +1729,8 @@ namespace TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: a buffer bound once at a block's point serves every program after it", "[llglslshader_gl]") { setUpCompute(); - UNSCOPED_INFO("first-only builds"); - CHECK(buildProgram(mCompute, "first only (test)", { { "test/firstOnlyC.glsl", GL_COMPUTE_SHADER } })); - UNSCOPED_INFO("first-and-counts builds"); - CHECK(buildProgram(mCompute2, "first and counts (test)", { { "test/firstAndCountsC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mCompute, "first only (test)", { { "test/firstOnlyC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mCompute2, "first and counts (test)", { { "test/firstAndCountsC.glsl", GL_COMPUTE_SHADER } })); const S32 first = mShaders.getStorageBlockBinding(TEST_BLOCK_FIRST); const S32 counts = mShaders.getStorageBlockBinding(TEST_COUNTS); UNSCOPED_INFO("the first program's block at the table's point"); @@ -1837,10 +1741,8 @@ namespace CHECK(first != counts); const std::vector zeros(4, 0u); - UNSCOPED_INFO("first allocates"); - CHECK(mBuffers[0].allocate(4 * sizeof(U32), 0, zeros.data(), "first (test)")); - UNSCOPED_INFO("counts allocate"); - CHECK(mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "counts (test)")); + REQUIRE(mBuffers[0].allocate(4 * sizeof(U32), 0, zeros.data(), "first (test)")); + REQUIRE(mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "counts (test)")); mCompute.bind(); gGL.bindStorageBuffer((U32)first, mBuffers[0]); @@ -1877,7 +1779,6 @@ namespace const size_t alignment = (size_t)gGLManager.mShaderStorageBufferOffsetAlignment; const size_t offset = (16 + alignment - 1) / alignment * alignment; ALGPUBuffer wide; - UNSCOPED_INFO("wide allocates"); REQUIRE(wide.allocate(offset + 4 * sizeof(U32), 0, nullptr, "wide (test)")); wide.clear(); mCompute.bind(); @@ -1894,7 +1795,6 @@ namespace wide.release(); UNSCOPED_INFO("a released buffer leaves its point empty"); CHECK(gGL.getStorageBuffer((U32)first) == 0u); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -1904,10 +1804,8 @@ namespace TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: a block array takes consecutive points and binds buffers by element", "[llglslshader_gl]") { setUpCompute(); - UNSCOPED_INFO("array program builds"); - CHECK(buildProgram(mCompute, "block array (test)", { { "test/blockArrayC.glsl", GL_COMPUTE_SHADER } })); - UNSCOPED_INFO("second-element program builds"); - CHECK(buildProgram(mCompute2, "block array one (test)", { { "test/blockArrayOneC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mCompute, "block array (test)", { { "test/blockArrayC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mCompute2, "block array one (test)", { { "test/blockArrayOneC.glsl", GL_COMPUTE_SHADER } })); const S32 base = mShaders.getStorageBlockBinding(TEST_BLOCK_ARRAY); UNSCOPED_INFO("the table gives the array a point"); CHECK(base >= 0); @@ -1918,14 +1816,11 @@ namespace UNSCOPED_INFO("no element 2"); CHECK(mCompute.getStorageBinding(TEST_BLOCK_ARRAY, 2) == -1); UNSCOPED_INFO("the second-element program's element 1 there too"); - CHECK(mCompute2.getStorageBinding(TEST_BLOCK_ARRAY, 1) == - base + 1); + CHECK(mCompute2.getStorageBinding(TEST_BLOCK_ARRAY, 1) == base + 1); const std::vector zeros(4, 0u); - UNSCOPED_INFO("a allocates"); - CHECK(mBuffers[0].allocate(4 * sizeof(U32), 0, zeros.data(), "element 0 (test)")); - UNSCOPED_INFO("b allocates"); - CHECK(mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "element 1 (test)")); + REQUIRE(mBuffers[0].allocate(4 * sizeof(U32), 0, zeros.data(), "element 0 (test)")); + REQUIRE(mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "element 1 (test)")); mCompute.bind(); mCompute.bindStorageBuffer(TEST_BLOCK_ARRAY, mBuffers[0], 0); @@ -1942,7 +1837,6 @@ namespace CHECK(readWords(mBuffers[1], 1)[0] == 33u); UNSCOPED_INFO("and not element 0's"); CHECK(readWords(mBuffers[0], 1)[0] == 11u); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -1953,8 +1847,7 @@ namespace { setUpCompute(); constexpr U32 SIDE = 8; - UNSCOPED_INFO("image array builds"); - CHECK(buildProgram(mCompute, "image array (test)", { { "test/imageArrayC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mCompute, "image array (test)", { { "test/imageArrayC.glsl", GL_COMPUTE_SHADER } })); UNSCOPED_INFO("element 0 first"); CHECK(mCompute.getImageUnit(TEST_IMAGES, 0) == 0); UNSCOPED_INFO("element 1 next"); @@ -1964,15 +1857,12 @@ namespace for (U32 i = 0; i < 3; ++i) { - UNSCOPED_INFO("target allocates"); + CAPTURE(i); REQUIRE(mTargets[i].allocate(SIDE, SIDE, GL_R32F)); } mCompute.bind(); - UNSCOPED_INFO("A bound"); CHECK(mCompute.bindImage(TEST_IMAGE_A, &mTargets[0], ALImageSlot::WRITE_ONLY) == 2); - UNSCOPED_INFO("element 0 bound"); CHECK(mCompute.bindImage(TEST_IMAGES, &mTargets[1], ALImageSlot::WRITE_ONLY, 0, 0, 0) == 0); - UNSCOPED_INFO("element 1 bound"); CHECK(mCompute.bindImage(TEST_IMAGES, &mTargets[2], ALImageSlot::WRITE_ONLY, 0, 0, 1) == 1); mCompute.dispatchThreads(SIDE, SIDE); gGL.memoryBarrier(LLRender::BARRIER_TEXTURE_UPDATE); @@ -1985,11 +1875,10 @@ namespace const std::vector held = readTexture(mTargets[t].getTexture(), SIDE, SIDE, 1, GL_RED, GL_FLOAT); for (U32 i = 0; i < SIDE * SIDE; ++i) { - UNSCOPED_INFO("target " + std::to_string(t) + " at " + std::to_string(i)); + CAPTURE(t, i); REQUIRE(held[i] == expected[t]); } } - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -1999,12 +1888,9 @@ namespace { setUpCompute(); constexpr U32 SIDE = 4; - UNSCOPED_INFO("signed image builds"); - CHECK(buildProgram(mCompute, "int image (test)", { { "test/imageIntC.glsl", GL_COMPUTE_SHADER } })); - UNSCOPED_INFO("float target allocates"); - CHECK(mTargets[0].allocate(SIDE, SIDE, GL_R32F)); - UNSCOPED_INFO("signed target allocates"); - CHECK(mTargets[1].allocate(SIDE, SIDE, GL_R32I)); + REQUIRE(buildProgram(mCompute, "int image (test)", { { "test/imageIntC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(mTargets[0].allocate(SIDE, SIDE, GL_R32F)); + REQUIRE(mTargets[1].allocate(SIDE, SIDE, GL_R32I)); mCompute.bind(); gDebugGL = true; @@ -2022,7 +1908,6 @@ namespace CHECK_FALSE(float_valid); UNSCOPED_INFO("over a signed format it passes"); CHECK(signed_valid); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -2036,12 +1921,10 @@ namespace { SKIP("the context has no stencil texturing"); } - UNSCOPED_INFO("unsigned reader builds"); - CHECK(buildProgram(mDraw, "stencil read (test)", - { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/intSampleF.glsl", GL_FRAGMENT_SHADER } })); - UNSCOPED_INFO("float reader builds"); - CHECK(buildProgram(mDraw2, "depth read (test)", - { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/sampleF.glsl", GL_FRAGMENT_SHADER } })); + REQUIRE(buildProgram(mDraw, "stencil read (test)", + { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/intSampleF.glsl", GL_FRAGMENT_SHADER } })); + REQUIRE(buildProgram(mDraw2, "depth read (test)", + { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/sampleF.glsl", GL_FRAGMENT_SHADER } })); GLuint texture = 0; glGenTextures(1, &texture); @@ -2078,7 +1961,6 @@ namespace CHECK(validates(mDraw2, TEST_SOURCE_MAP, GL_DEPTH_COMPONENT)); glDeleteTextures(1, &texture); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } @@ -2088,9 +1970,8 @@ namespace TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: every kind of sampler takes a texture unit in reserved order", "[llglslshader_gl]") { setUpShaders(); - UNSCOPED_INFO("sampler types build"); - CHECK(buildProgram(mDraw, "sampler types (test)", - { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/samplerTypesF.glsl", GL_FRAGMENT_SHADER } })); + REQUIRE(buildProgram(mDraw, "sampler types (test)", + { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/samplerTypesF.glsl", GL_FRAGMENT_SHADER } })); UNSCOPED_INFO("the array first"); CHECK(mDraw.getTextureChannel(TEST_SAMPLERS) == 0); UNSCOPED_INFO("the array texture after its two units"); @@ -2107,7 +1988,6 @@ namespace CHECK(units[0] == 0); UNSCOPED_INFO("its second at the next"); CHECK(units[1] == 1); - UNSCOPED_INFO("no GL error"); CHECK(glGetError() == GLenum(GL_NO_ERROR)); } } diff --git a/indra/llrender/tests/llimagegl_test.cpp b/indra/llrender/tests/llimagegl_test.cpp index cb06a60d99..98f1771776 100644 --- a/indra/llrender/tests/llimagegl_test.cpp +++ b/indra/llrender/tests/llimagegl_test.cpp @@ -105,11 +105,8 @@ namespace LLPointer raw = makeRaw(16, 16, 4, /*fill=*/0x80); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - UNSCOPED_INFO("createGLTexture succeeded"); CHECK(img->createGLTexture(/*discard_level=*/0, raw.get())); - UNSCOPED_INFO("texname allocated"); CHECK(img->getTexName() != 0); - UNSCOPED_INFO("getHasGLTexture reports true"); CHECK(img->getHasGLTexture()); } @@ -120,16 +117,12 @@ namespace { LLPointer raw = makeRaw(16, 16, 4, /*fill=*/0x80); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - UNSCOPED_INFO("createGLTexture succeeded"); CHECK(img->createGLTexture(0, raw.get())); - UNSCOPED_INFO("texname allocated before destroy"); CHECK(img->getTexName() != 0); img->destroyGLTexture(); - UNSCOPED_INFO("texname cleared after destroy"); CHECK((S32)img->getTexName() == 0); - UNSCOPED_INFO("getHasGLTexture false after destroy"); CHECK_FALSE(img->getHasGLTexture()); } @@ -142,8 +135,7 @@ namespace { LLPointer raw = makeRaw(32, 32, 4, /*fill=*/0x80); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - UNSCOPED_INFO("createGLTexture succeeded"); - CHECK(img->createGLTexture(0, raw.get())); + REQUIRE(img->createGLTexture(0, raw.get())); // Bind the new texture to unit 0 the same way the renderer // would, then snapshot what GL_TEXTURE_BINDING_2D points at. @@ -152,7 +144,6 @@ namespace GLint bound_before = 0; glGetIntegerv(GL_TEXTURE_BINDING_2D, &bound_before); - UNSCOPED_INFO("texture is bound to unit 0 before setSubImage"); CHECK((S32)bound_before == (S32)img->getTexName()); // Partial-rect upload — this is the path that used to @@ -162,7 +153,6 @@ namespace // offset, so a 32x32 source matching the destination is // simplest. LLPointer patch = makeRaw(32, 32, 4, /*fill=*/0xFF); - UNSCOPED_INFO("setSubImage succeeded"); CHECK(img->setSubImage(patch.get(), /*x_pos=*/4, /*y_pos=*/4, /*width=*/8, /*height=*/8, @@ -171,7 +161,6 @@ namespace glActiveTexture(GL_TEXTURE0); GLint bound_after = 0; glGetIntegerv(GL_TEXTURE_BINDING_2D, &bound_after); - UNSCOPED_INFO("texture still bound to unit 0 after setSubImage"); CHECK((S32)bound_after == (S32)img->getTexName()); } @@ -190,23 +179,19 @@ namespace LLPointer la = new LLImageGL(/*usemipmaps=*/false); la->setExplicitFormat(GL_LUMINANCE_ALPHA, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE); - UNSCOPED_INFO("LUMINANCE_ALPHA primary rewrites to GL_RG"); CHECK((S32)la->getPrimaryFormat() == (S32)GL_RG); LLPointer lum = new LLImageGL(/*usemipmaps=*/false); lum->setExplicitFormat(GL_LUMINANCE, GL_LUMINANCE, GL_UNSIGNED_BYTE); - UNSCOPED_INFO("LUMINANCE primary rewrites to GL_RED"); CHECK((S32)lum->getPrimaryFormat() == (S32)GL_RED); LLPointer alpha = new LLImageGL(/*usemipmaps=*/false); alpha->setExplicitFormat(GL_ALPHA, GL_ALPHA, GL_UNSIGNED_BYTE); - UNSCOPED_INFO("ALPHA primary rewrites to GL_RED"); CHECK((S32)alpha->getPrimaryFormat() == (S32)GL_RED); // Modern formats shouldn't be touched. LLPointer rgba = new LLImageGL(/*usemipmaps=*/false); rgba->setExplicitFormat(GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE); - UNSCOPED_INFO("RGBA primary preserved"); CHECK((S32)rgba->getPrimaryFormat() == (S32)GL_RGBA); } @@ -217,59 +202,43 @@ namespace // upload paths exercise. TEST_CASE_METHOD(llimagegl_data, "LLImageGL: dataFormatBits/Bytes/Components give the documented host and VRAM sizes", "[LLImageGL]") { - UNSCOPED_INFO("RGBA8 -> 32 bits"); CHECK(LLImageGL::dataFormatBits(GL_RGBA8) == 32); - UNSCOPED_INFO("RG8 -> 16 bits"); CHECK(LLImageGL::dataFormatBits(GL_RG8) == 16); - UNSCOPED_INFO("R8 -> 8 bits"); CHECK(LLImageGL::dataFormatBits(GL_R8) == 8); - UNSCOPED_INFO("RGBA16F -> 64 bits"); CHECK(LLImageGL::dataFormatBits(GL_RGBA16F) == 64); - UNSCOPED_INFO("RGBA components == 4"); CHECK(LLImageGL::dataFormatComponents(GL_RGBA) == 4); - UNSCOPED_INFO("LUMINANCE_ALPHA components == 2"); CHECK(LLImageGL::dataFormatComponents(GL_LUMINANCE_ALPHA) == 2); - UNSCOPED_INFO("RED components == 1"); CHECK(LLImageGL::dataFormatComponents(GL_RED) == 1); // 16x16 RGBA8 = 16*16*4 = 1024 bytes, already 4-aligned. - UNSCOPED_INFO("16x16 RGBA8 = 1024 bytes"); CHECK((S32)LLImageGL::dataFormatBytes(GL_RGBA8, 16, 16) == 1024); // 5x3 R8 = 15 bytes; rounds up to 16 (4-aligned). - UNSCOPED_INFO("5x3 R8 byte count rounds to 4-aligned 16"); CHECK((S32)LLImageGL::dataFormatBytes(GL_R8, 5, 3) == 16); // Host vs. VRAM split: dataFormatBits returns the tight host // layout (used for CPU upload-buffer math); dataFormatVRAMBits // returns the padded driver allocation (used for VRAM stats). // For formats drivers don't pad, the two agree. - UNSCOPED_INFO("RGB8 host -> 24 bits (tight)"); CHECK(LLImageGL::dataFormatBits(GL_RGB8) == 24); - UNSCOPED_INFO("RGB8 VRAM -> 32 bits (padded to RGBX)"); + UNSCOPED_INFO("padded to RGBX"); CHECK(LLImageGL::dataFormatVRAMBits(GL_RGB8) == 32); - UNSCOPED_INFO("RGB16F host -> 48 bits (tight)"); CHECK(LLImageGL::dataFormatBits(GL_RGB16F) == 48); - UNSCOPED_INFO("RGB16F VRAM -> 64 bits (padded to RGBA16F)"); + UNSCOPED_INFO("padded to RGBA16F"); CHECK(LLImageGL::dataFormatVRAMBits(GL_RGB16F) == 64); - UNSCOPED_INFO("RGB32F host -> 96 bits (tight)"); CHECK(LLImageGL::dataFormatBits(GL_RGB32F) == 96); - UNSCOPED_INFO("RGB32F VRAM -> 128 bits (padded to RGBA32F)"); + UNSCOPED_INFO("padded to RGBA32F"); CHECK(LLImageGL::dataFormatVRAMBits(GL_RGB32F) == 128); - UNSCOPED_INFO("DEPTH_COMPONENT24 host -> 24 bits (tight)"); CHECK(LLImageGL::dataFormatBits(GL_DEPTH_COMPONENT24) == 24); - UNSCOPED_INFO("DEPTH_COMPONENT24 VRAM -> 32 bits (padded)"); CHECK(LLImageGL::dataFormatVRAMBits(GL_DEPTH_COMPONENT24) == 32); // VRAM bytes path applies the same 4-byte alignment as the // host bytes path and routes unpadded formats through the // host table — so RGBA8 matches between the two. - UNSCOPED_INFO("16x16 RGBA8 VRAM = 1024 bytes (matches host)"); + UNSCOPED_INFO("matches the host bytes"); CHECK((S32)LLImageGL::dataFormatVRAMBytes(GL_RGBA8, 16, 16) == 1024); // 16x16 RGB8: host 16*16*3 = 768; VRAM 16*16*4 = 1024. - UNSCOPED_INFO("16x16 RGB8 host = 768 bytes"); CHECK((S32)LLImageGL::dataFormatBytes(GL_RGB8, 16, 16) == 768); - UNSCOPED_INFO("16x16 RGB8 VRAM = 1024 bytes (padded)"); CHECK((S32)LLImageGL::dataFormatVRAMBytes(GL_RGB8, 16, 16) == 1024); } @@ -290,16 +259,12 @@ namespace // to 2 component"). LLPointer raw2 = makeRaw(8, 8, 2, /*fill=*/0x80); LLPointer img2 = new LLImageGL(/*usemipmaps=*/false); - UNSCOPED_INFO("createGLTexture 2-component succeeded"); CHECK(img2->createGLTexture(0, raw2.get())); - UNSCOPED_INFO("2 components -> GL_RG primary"); CHECK((S32)img2->getPrimaryFormat() == (S32)GL_RG); LLPointer raw4 = makeRaw(8, 8, 4, /*fill=*/0x80); LLPointer img4 = new LLImageGL(/*usemipmaps=*/false); - UNSCOPED_INFO("createGLTexture 4-component succeeded"); CHECK(img4->createGLTexture(0, raw4.get())); - UNSCOPED_INFO("4 components -> GL_RGBA primary"); CHECK((S32)img4->getPrimaryFormat() == (S32)GL_RGBA); } @@ -311,9 +276,7 @@ namespace { LLPointer raw = makeRaw(8, 8, 4, /*fill=*/0x80); LLPointer img = new LLImageGL(/*usemipmaps=*/true); - UNSCOPED_INFO("createGLTexture mipmapped succeeded"); - CHECK(img->createGLTexture(0, raw.get())); - UNSCOPED_INFO("getUseMipMaps reports true"); + REQUIRE(img->createGLTexture(0, raw.get())); CHECK(img->getUseMipMaps()); glActiveTexture(GL_TEXTURE0); @@ -321,7 +284,6 @@ namespace GLint mip1_w = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 1, GL_TEXTURE_WIDTH, &mip1_w); - UNSCOPED_INFO("mip 1 width == 4 for an 8x8 base"); CHECK((S32)mip1_w == 4); } @@ -343,8 +305,7 @@ namespace // createGLTexture then applies the matching swizzle. LLPointer raw = makeRaw(8, 8, 2, /*fill=*/0x80); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - UNSCOPED_INFO("createGLTexture 2-component succeeded"); - CHECK(img->createGLTexture(0, raw.get())); + REQUIRE(img->createGLTexture(0, raw.get())); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, img->getTexName()); @@ -352,20 +313,17 @@ namespace GLint mask[4] = { 0, 0, 0, 0 }; glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA, mask); - UNSCOPED_INFO("R swizzle == GL_RED"); CHECK((S32)mask[0] == (S32)GL_RED); - UNSCOPED_INFO("G swizzle == GL_RED"); CHECK((S32)mask[1] == (S32)GL_RED); - UNSCOPED_INFO("B swizzle == GL_RED"); CHECK((S32)mask[2] == (S32)GL_RED); - UNSCOPED_INFO("A swizzle == GL_GREEN"); CHECK((S32)mask[3] == (S32)GL_GREEN); } // Round-trip: upload a known pattern, glGetTexImage it back, and // verify byte-for-byte. Covers the createGLTexture upload path and // readBackRaw together. RGBA chosen so no deprecated rewrite kicks - // in — that path is covered separately by test<4> and test<6>. + // in — that path is covered separately by the setExplicitFormat and + // component-count cases above. TEST_CASE_METHOD(llimagegl_data, "LLImageGL: readBackRaw returns the bytes createGLTexture uploaded", "[LLImageGL]") { constexpr U16 W = 8, H = 8; @@ -378,15 +336,12 @@ namespace sd[i] = (U8)(i & 0xFF); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - UNSCOPED_INFO("createGLTexture succeeded"); - CHECK(img->createGLTexture(0, src.get())); + REQUIRE(img->createGLTexture(0, src.get())); LLPointer dst = new LLImageRaw(W, H, C); std::memset(dst->getData(), 0, (size_t)W * H * C); - UNSCOPED_INFO("readBackRaw succeeded"); CHECK(img->readBackRaw(0, dst.get(), /*compressed_ok=*/false)); - UNSCOPED_INFO("readback bytes match upload"); CHECK(std::memcmp(src->getData(), dst->getData(), (size_t)W * H * C) == 0); @@ -409,20 +364,18 @@ namespace LLPointer img = new LLImageGL(/*usemipmaps=*/false); img->setExplicitFormat(GL_RGBA8, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV); - UNSCOPED_INFO("createGLTexture succeeded"); - CHECK(img->createGLTexture(0, src.get())); + REQUIRE(img->createGLTexture(0, src.get())); std::vector got((size_t)W * H * 4); readTexture(img, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, got.data()); - UNSCOPED_INFO("full upload reads back"); - REQUIRE(std::memcmp(got.data(), sd, got.size()) == 0); + UNSCOPED_INFO("the full upload"); + CHECK(std::memcmp(got.data(), sd, got.size()) == 0); // A partial-width update, which slices one row at a time. LLPointer patch = new LLImageRaw(W, H, 4); U8* pd = patch->getData(); for (size_t i = 0; i < (size_t)W * H * 4; ++i) pd[i] = (U8)(0xFF - (i & 0xFF)); - UNSCOPED_INFO("setSubImage succeeded"); CHECK(img->setSubImage(patch.get(), /*x_pos=*/8, /*y_pos=*/4, /*width=*/16, /*height=*/8, /*force_fast_update=*/true)); @@ -459,33 +412,25 @@ namespace } LLPointer img = new LLImageGL(/*usemipmaps=*/true); - UNSCOPED_INFO("createGLTexture succeeded"); - CHECK(img->createGLTexture(0, raw.get())); + REQUIRE(img->createGLTexture(0, raw.get())); const U32 method = gGLManager.mDownScaleMethod; gGLManager.mDownScaleMethod = 0; const bool scaled = img->scaleDown(1); gGLManager.mDownScaleMethod = method; - UNSCOPED_INFO("scaleDown succeeded"); CHECK(scaled); - UNSCOPED_INFO("discard level follows"); CHECK(img->getDiscardLevel() == 1); bindForRead(img); GLint w = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); - UNSCOPED_INFO("the new texture is half size"); - CHECK((S32)w == (S32)W / 2); + REQUIRE((S32)w == (S32)W / 2); GLint mask[4] = { 0, 0, 0, 0 }; glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA, mask); - UNSCOPED_INFO("R swizzle == GL_RED"); CHECK((S32)mask[0] == (S32)GL_RED); - UNSCOPED_INFO("G swizzle == GL_RED"); CHECK((S32)mask[1] == (S32)GL_RED); - UNSCOPED_INFO("B swizzle == GL_RED"); CHECK((S32)mask[2] == (S32)GL_RED); - UNSCOPED_INFO("A swizzle == GL_GREEN"); CHECK((S32)mask[3] == (S32)GL_GREEN); std::vector got((size_t)(W / 2) * (H / 2) * 2); @@ -505,8 +450,7 @@ namespace TEST_CASE_METHOD(llimagegl_data, "LLImageGL: scaleDown waits until an upload in flight has ended", "[LLImageGL]") { LLPointer img = new LLImageGL(/*usemipmaps=*/true); - UNSCOPED_INFO("createGLTexture succeeded"); - CHECK(img->createGLTexture(0, makeRaw(16, 16, 4, 0x80).get())); + REQUIRE(img->createGLTexture(0, makeRaw(16, 16, 4, 0x80).get())); const U32 name = img->getTexName(); // The PBO copy needs no framebuffer of its own. @@ -523,7 +467,6 @@ namespace UNSCOPED_INFO("refused while an upload is in flight"); CHECK_FALSE(during); - UNSCOPED_INFO("texture untouched meanwhile"); CHECK((S32)name_during == (S32)name); UNSCOPED_INFO("done once the upload has ended"); CHECK(after); @@ -543,9 +486,7 @@ namespace sd[i] = (U8)(i & 0xFF); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - UNSCOPED_INFO("createGLTexture succeeded"); - CHECK(img->createGLTexture(2, src.get())); - UNSCOPED_INFO("on screen at 16 wide"); + REQUIRE(img->createGLTexture(2, src.get())); CHECK(img->getWidth() == 16); img->beginUpload(); @@ -557,10 +498,9 @@ namespace img->setDiscardLevel(2); img->endUpload(); - UNSCOPED_INFO("readBackRaw succeeded"); - CHECK(read); + REQUIRE(read); UNSCOPED_INFO("read at the size on screen"); - CHECK((S32)dst->getWidth() == (S32)W); + REQUIRE((S32)dst->getWidth() == (S32)W); UNSCOPED_INFO("and reads what is on screen"); CHECK(std::memcmp(dst->getData(), sd, (size_t)W * H * 4) == 0); } @@ -573,8 +513,7 @@ namespace { constexpr U16 W = 16, H = 16; LLPointer img = new LLImageGL(/*usemipmaps=*/false); - UNSCOPED_INFO("createGLTexture succeeded"); - CHECK(img->createGLTexture(0, makeRaw(W, H, 4, 0xFF).get())); + REQUIRE(img->createGLTexture(0, makeRaw(W, H, 4, 0xFF).get())); const LLVector2 centre(0.5f, 0.5f); UNSCOPED_INFO("opaque texture picks"); CHECK(img->getMask(centre)); @@ -605,31 +544,27 @@ namespace constexpr U16 W = 32, H = 32; LLPointer a = new LLImageGL(/*usemipmaps=*/false); LLPointer b = new LLImageGL(/*usemipmaps=*/false); - UNSCOPED_INFO("create a"); - CHECK(a->createGLTexture(0, makeRaw(W, H, 4, 0x10).get())); - UNSCOPED_INFO("create b"); - CHECK(b->createGLTexture(0, makeRaw(W, H, 4, 0x20).get())); + REQUIRE(a->createGLTexture(0, makeRaw(W, H, 4, 0x10).get())); + REQUIRE(b->createGLTexture(0, makeRaw(W, H, 4, 0x20).get())); // Partial update, as the glyph atlas does it. Forcing the slot 1 bind is what // leaves unit 1 active, cached or not. gGL.getTextureSlot(0)->bind(a.get()); gGL.getTextureSlot(1)->bind(b.get(), false, /*forceBind=*/true); - UNSCOPED_INFO("setSubImage succeeded"); CHECK(a->setSubImage(makeRaw(W, H, 4, 0xFF).get(), 4, 4, 8, 8, /*force_fast_update=*/true)); UNSCOPED_INFO("the update reached a"); CHECK(readTexelRGBA(a, 5, 5) == 0xFFFFFFFFu); - UNSCOPED_INFO("and not b"); + UNSCOPED_INFO("the update left b alone"); CHECK(readTexelRGBA(b, 5, 5) == 0x20202020u); // Re-upload in place, through setImage. gGL.getTextureSlot(0)->bind(a.get()); gGL.getTextureSlot(1)->bind(b.get(), false, /*forceBind=*/true); - UNSCOPED_INFO("re-upload succeeded"); - CHECK(a->createGLTexture(0, makeRaw(W, H, 4, 0x77).get())); + REQUIRE(a->createGLTexture(0, makeRaw(W, H, 4, 0x77).get())); UNSCOPED_INFO("the re-upload reached a"); CHECK(readTexelRGBA(a, 5, 5) == 0x77777777u); - UNSCOPED_INFO("and not b"); + UNSCOPED_INFO("the re-upload left b alone"); CHECK(readTexelRGBA(b, 5, 5) == 0x20202020u); } @@ -640,32 +575,28 @@ namespace TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a re-upload at the same discard level takes a new size or format", "[LLImageGL]") { LLPointer img = new LLImageGL(/*usemipmaps=*/false); - UNSCOPED_INFO("create 32x32"); - CHECK(img->createGLTexture(0, makeRaw(32, 32, 4, 0x11).get())); - UNSCOPED_INFO("re-create 64x64"); - CHECK(img->createGLTexture(0, makeRaw(64, 64, 4, 0x22).get())); + REQUIRE(img->createGLTexture(0, makeRaw(32, 32, 4, 0x11).get())); + REQUIRE(img->createGLTexture(0, makeRaw(64, 64, 4, 0x22).get())); bindForRead(img); GLint w = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); UNSCOPED_INFO("the texture took the new size"); CHECK((S32)w == 64); - UNSCOPED_INFO("and the new pixels"); + UNSCOPED_INFO("the resized texture holds the new pixels"); CHECK(readTexelRGBA(img, 40, 40) == 0x22222222u); // Same size, an alpha channel the old storage has no room for. LLPointer rgb = new LLImageGL(/*usemipmaps=*/false); - UNSCOPED_INFO("create RGB"); - CHECK(rgb->createGLTexture(0, makeRaw(16, 16, 3, 0x33).get())); - UNSCOPED_INFO("re-create RGBA"); - CHECK(rgb->createGLTexture(0, makeRaw(16, 16, 4, 0x44).get())); + REQUIRE(rgb->createGLTexture(0, makeRaw(16, 16, 3, 0x33).get())); + REQUIRE(rgb->createGLTexture(0, makeRaw(16, 16, 4, 0x44).get())); bindForRead(rgb); GLint format = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &format); UNSCOPED_INFO("the texture took the new format"); CHECK((S32)format == (S32)GL_SRGB8_ALPHA8); - UNSCOPED_INFO("and the new pixels"); + UNSCOPED_INFO("the reformatted texture holds the new pixels"); CHECK(readTexelRGBA(rgb, 3, 3) == 0x44444444u); } @@ -676,12 +607,10 @@ namespace TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a slot's bind cache forgets a deleted texture's name", "[LLImageGL]") { LLPointer img = new LLImageGL(/*usemipmaps=*/false); - UNSCOPED_INFO("createGLTexture succeeded"); - CHECK(img->createGLTexture(0, makeRaw(16, 16, 4, 0x80).get())); + REQUIRE(img->createGLTexture(0, makeRaw(16, 16, 4, 0x80).get())); const U32 name = img->getTexName(); gGL.getTextureSlot(3)->bind(img.get()); - UNSCOPED_INFO("slot 3 caches it"); CHECK(gGL.getTextureSlot(3)->getCurrTexture() == name); img->destroyGLTexture(); @@ -691,7 +620,6 @@ namespace LLImageGL::updateClass(); } - UNSCOPED_INFO("the cache let go of the deleted name"); CHECK(gGL.getTextureSlot(3)->getCurrTexture() == 0u); } @@ -702,8 +630,7 @@ namespace { constexpr U16 W = 256, H = 256; LLPointer img = new LLImageGL(/*usemipmaps=*/true); - UNSCOPED_INFO("createGLTexture succeeded"); - CHECK(img->createGLTexture(0, makeRaw(W, H, 4, 0x80).get())); + REQUIRE(img->createGLTexture(0, makeRaw(W, H, 4, 0x80).get())); bindForRead(img); GLint max_level = 0; @@ -715,7 +642,7 @@ namespace U8 px[4] = { 0, 0, 0, 0 }; glGetTexImage(GL_TEXTURE_2D, last, GL_RGBA, GL_UNSIGNED_BYTE, px); { - INFO("and the 1x1 level was generated"); + INFO("the 1x1 level was generated"); CHECK(px[0] == 0x80); CHECK(px[1] == 0x80); CHECK(px[2] == 0x80); @@ -731,8 +658,7 @@ namespace { { LLPointer img = new LLImageGL(/*usemipmaps=*/false); - UNSCOPED_INFO("createGLTexture succeeded"); - CHECK(img->createGLTexture(0, makeRaw(16, 16, 4, 0x80).get())); + REQUIRE(img->createGLTexture(0, makeRaw(16, 16, 4, 0x80).get())); gGL.getTextureSlot(2)->bind(img.get()); } @@ -740,14 +666,11 @@ namespace gl.reset(); gl = std::make_unique(); - UNSCOPED_INFO("unit 2's cache starts empty"); CHECK(gGL.getTextureSlot(2)->getCurrTexture() == 0u); - UNSCOPED_INFO("and unit 0 is the active one"); CHECK(gGL.getCurrentTexUnitIndex() == 0u); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - UNSCOPED_INFO("createGLTexture succeeded in the new context"); - CHECK(img->createGLTexture(0, makeRaw(16, 16, 4, 0x40).get())); + REQUIRE(img->createGLTexture(0, makeRaw(16, 16, 4, 0x40).get())); gGL.getTextureSlot(2)->bind(img.get()); GLint bound = 0; glGetIntegerv(GL_TEXTURE_BINDING_2D, &bound); @@ -772,10 +695,11 @@ namespace GLint dst = 0; glGetIntegerv(GL_BLEND_SRC_RGB, &src); glGetIntegerv(GL_BLEND_DST_RGB, &dst); - UNSCOPED_INFO("init's alpha blending reached GL: source"); - CHECK((S32)src == (S32)GL_SRC_ALPHA); - UNSCOPED_INFO("init's alpha blending reached GL: destination"); - CHECK((S32)dst == (S32)GL_ONE_MINUS_SRC_ALPHA); + { + INFO("init's alpha blending reached GL"); + CHECK((S32)src == (S32)GL_SRC_ALPHA); + CHECK((S32)dst == (S32)GL_ONE_MINUS_SRC_ALPHA); + } bool mask[4] = { false, false, false, false }; gGL.getColorMask(mask); @@ -789,7 +713,7 @@ namespace GLboolean gl_mask[4] = { GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE }; glGetBooleanv(GL_COLOR_WRITEMASK, gl_mask); { - INFO("and GL agrees"); + INFO("and GL's mask agrees"); CHECK(gl_mask[0]); CHECK(gl_mask[1]); CHECK(gl_mask[2]); @@ -810,10 +734,9 @@ namespace // after the unmap is the name this buffer drew from the pool. auto fill = [](LLVertexBuffer* vb, const std::string& when) { - UNSCOPED_INFO("allocateBuffer succeeded " + when); + CAPTURE(when); REQUIRE(vb->allocateBuffer(3, 0)); LLStrider verts; - UNSCOPED_INFO("getVertexStrider succeeded " + when); REQUIRE(vb->getVertexStrider(verts)); for (S32 i = 0; i < 3; ++i) { @@ -836,9 +759,8 @@ namespace fill(vb, "in the new context"); GLint bound = 0; glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &bound); - UNSCOPED_INFO("a buffer is bound"); CHECK(bound != 0); - UNSCOPED_INFO("and it is one this context made"); + UNSCOPED_INFO("it is a buffer this context made"); CHECK(glIsBuffer((GLuint)bound) == GL_TRUE); LLVertexBuffer::unbind(); } @@ -864,8 +786,7 @@ namespace } thread_end; LLPointer img = new LLImageGL(/*usemipmaps=*/false); - UNSCOPED_INFO("createGLTexture succeeded"); - CHECK(img->createGLTexture(0, makeRaw(16, 16, 4, 0x11).get())); + REQUIRE(img->createGLTexture(0, makeRaw(16, 16, 4, 0x11).get())); const U32 old_name = img->getTexName(); LLPointer raw = makeRaw(32, 32, 4, 0x55); @@ -887,9 +808,8 @@ namespace } // The handover was posted before worker_done was raised. mainloop.runPending(); - UNSCOPED_INFO("the worker finished"); CHECK(worker_done.load()); - UNSCOPED_INFO("and its createGLTexture succeeded"); + UNSCOPED_INFO("the worker's createGLTexture succeeded"); CHECK(worker_ok.load()); bool completed = false; @@ -904,7 +824,7 @@ namespace CHECK_FALSE(completed); UNSCOPED_INFO("the texture on screen is the old one"); CHECK(img->getTexName() == old_name); - UNSCOPED_INFO("at its own size"); + UNSCOPED_INFO("still at the old size"); CHECK(img->getWidth() == 16); while (LLImageGL::publishUploads() > 0 && std::chrono::steady_clock::now() < deadline) @@ -914,15 +834,15 @@ namespace LLImageGLThread::deleteSingleton(); - UNSCOPED_INFO("the completion ran"); + UNSCOPED_INFO("the afterPublish completion ran"); CHECK(completed); UNSCOPED_INFO("after the new texture published"); CHECK(name_at_completion != old_name); - UNSCOPED_INFO("which is what the image names"); + UNSCOPED_INFO("the image names the published texture"); CHECK(img->getTexName() == name_at_completion); - UNSCOPED_INFO("at its new size"); + UNSCOPED_INFO("at the new size"); CHECK(img->getWidth() == 32); - UNSCOPED_INFO("holding the upload"); + UNSCOPED_INFO("holding the upload's pixels"); CHECK(readTexelRGBA(img, 5, 5) == 0x55555555u); } @@ -946,14 +866,11 @@ namespace GLfloat units = 0.f; glGetFloatv(GL_POLYGON_OFFSET_FACTOR, &factor); glGetFloatv(GL_POLYGON_OFFSET_UNITS, &units); - UNSCOPED_INFO("the polygon offset reached the new context: factor"); CHECK((F32)factor == sign * 1.f); - UNSCOPED_INFO("the polygon offset reached the new context: units"); CHECK((F32)units == sign * 2.f); GLint patch = 0; glGetIntegerv(GL_PATCH_VERTICES, &patch); - UNSCOPED_INFO("and the patch size"); CHECK((S32)patch == 4); } @@ -973,7 +890,6 @@ namespace glGetVertexAttribiv(LLVertexBuffer::TYPE_VERTEX, GL_VERTEX_ATTRIB_ARRAY_ENABLED, &enabled); LLVertexBuffer::setupClientArrays(0); - UNSCOPED_INFO("the position array is enabled on the new context"); CHECK(enabled == GL_TRUE); } @@ -985,7 +901,6 @@ namespace { LLWindow* window = gl->window(); void* shared = window->createSharedContext(); - UNSCOPED_INFO("a shared context was made"); REQUIRE(shared != nullptr); std::thread worker([window, shared] { @@ -1021,19 +936,16 @@ namespace LLPointer r32ui = new LLImageGL(W, H, 1, /*usemipmaps=*/true); r32ui->setExplicitFormat(GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT); U64 before = LLImageGL::getTextureBytesAllocated(); - UNSCOPED_INFO("R32UI created"); - CHECK(r32ui->createGLTexture(0, reinterpret_cast(uints.data()))); + REQUIRE(r32ui->createGLTexture(0, reinterpret_cast(uints.data()))); UNSCOPED_INFO("R32UI accounted at four bytes a texel"); CHECK((S64)(LLImageGL::getTextureBytesAllocated() - before) == (S64)N * 4); - UNSCOPED_INFO("and its host bytes"); CHECK((S64)r32ui->getMipBytes(0) == (S64)N * 4); - UNSCOPED_INFO("no mips asked of it"); + UNSCOPED_INFO("an integer texture asked for mips takes none"); CHECK_FALSE(r32ui->getUseMipMaps()); bindForRead(r32ui); GLint format = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &format); - UNSCOPED_INFO("R32UI storage"); CHECK((S32)format == (S32)GL_R32UI); GLint level1_width = -1; glGetTexLevelParameteriv(GL_TEXTURE_2D, 1, GL_TEXTURE_WIDTH, &level1_width); @@ -1042,7 +954,6 @@ namespace std::vector got_uints(N, 0); readTexture(r32ui, GL_RED_INTEGER, GL_UNSIGNED_INT, got_uints.data()); - UNSCOPED_INFO("R32UI holds the upload"); CHECK(got_uints == uints); // A partial update, its rows read from a wider source as the glyph atlas updates. @@ -1051,7 +962,6 @@ namespace { patch[i] = 0xF0000000u + (U32)i; } - UNSCOPED_INFO("R32UI partial update"); CHECK(r32ui->setSubImage(reinterpret_cast(patch.data()), W, H, 2, 1, 4, 3, /*force_fast_update=*/true)); readTexture(r32ui, GL_RED_INTEGER, GL_UNSIGNED_INT, got_uints.data()); for (U32 y = 0; y < H; ++y) @@ -1060,7 +970,7 @@ namespace { const size_t i = (size_t)y * W + x; const bool in_patch = x >= 2 && x < 6 && y >= 1 && y < 4; - UNSCOPED_INFO("R32UI texel " + std::to_string(x) + "," + std::to_string(y)); + CAPTURE(x, y); REQUIRE(got_uints[i] == (in_patch ? patch[i] : uints[i])); } @@ -1075,17 +985,14 @@ namespace LLPointer rg16i = new LLImageGL(W, H, 2, /*usemipmaps=*/false); rg16i->setExplicitFormat(GL_RG16I, GL_RG_INTEGER, GL_SHORT); before = LLImageGL::getTextureBytesAllocated(); - UNSCOPED_INFO("RG16I created"); - CHECK(rg16i->createGLTexture(0, reinterpret_cast(shorts.data()))); + REQUIRE(rg16i->createGLTexture(0, reinterpret_cast(shorts.data()))); UNSCOPED_INFO("RG16I accounted at four bytes a texel"); CHECK((S64)(LLImageGL::getTextureBytesAllocated() - before) == (S64)N * 4); bindForRead(rg16i); glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &format); - UNSCOPED_INFO("RG16I storage"); CHECK((S32)format == (S32)GL_RG16I); std::vector got_shorts(N * 2, 0); readTexture(rg16i, GL_RG_INTEGER, GL_SHORT, got_shorts.data()); - UNSCOPED_INFO("RG16I holds the upload"); CHECK(got_shorts == shorts); std::vector wide(N * 4); @@ -1099,27 +1006,21 @@ namespace LLPointer rgba32ui = new LLImageGL(W, H, 4, /*usemipmaps=*/false); rgba32ui->setExplicitFormat(GL_RGBA32UI, GL_RGBA_INTEGER, GL_UNSIGNED_INT); before = LLImageGL::getTextureBytesAllocated(); - UNSCOPED_INFO("RGBA32UI created"); - CHECK(rgba32ui->createGLTexture(0, reinterpret_cast(wide.data()))); + REQUIRE(rgba32ui->createGLTexture(0, reinterpret_cast(wide.data()))); UNSCOPED_INFO("RGBA32UI accounted at sixteen bytes a texel"); CHECK((S64)(LLImageGL::getTextureBytesAllocated() - before) == (S64)N * 16); bindForRead(rgba32ui); glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &format); - UNSCOPED_INFO("RGBA32UI storage"); CHECK((S32)format == (S32)GL_RGBA32UI); std::vector got_wide(N * 4, 0); readTexture(rgba32ui, GL_RGBA_INTEGER, GL_UNSIGNED_INT, got_wide.data()); - UNSCOPED_INFO("RGBA32UI holds the upload"); CHECK(got_wide == wide); // What an LLImageRaw cannot hold, readBackRaw does not read: it has a byte a component. LLPointer raw = new LLImageRaw(); - UNSCOPED_INFO("no byte image of R32UI"); CHECK_FALSE(r32ui->readBackRaw(0, raw.get(), /*compressed_ok=*/false)); - UNSCOPED_INFO("nor a downscale"); CHECK_FALSE(r32ui->scaleDown(1)); - UNSCOPED_INFO("no GL error"); CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } @@ -1139,11 +1040,9 @@ namespace }; for (const auto& entry : bits) { - const std::string name = "format " + std::to_string(entry.first); - UNSCOPED_INFO(name + " host bits"); + INFO("format " << entry.first); REQUIRE(LLImageGL::dataFormatBits(entry.first) == entry.second); const S32 vram = entry.second == 24 ? 32 : entry.second == 48 ? 64 : entry.second == 96 ? 128 : entry.second; - UNSCOPED_INFO(name + " VRAM bits"); REQUIRE(LLImageGL::dataFormatVRAMBits(entry.first) == vram); } @@ -1154,7 +1053,7 @@ namespace }; for (const auto& entry : components) { - UNSCOPED_INFO("pixel format " + std::to_string(entry.first) + " components"); + INFO("pixel format " << entry.first); REQUIRE(LLImageGL::dataFormatComponents(entry.first) == entry.second); } } diff --git a/indra/llrender/tests/llrender2dutils_gl_test.cpp b/indra/llrender/tests/llrender2dutils_gl_test.cpp index b7b9548d47..93baaf3531 100644 --- a/indra/llrender/tests/llrender2dutils_gl_test.cpp +++ b/indra/llrender/tests/llrender2dutils_gl_test.cpp @@ -119,9 +119,10 @@ namespace return b; } - static void ensureNear(const std::string& what, F32 actual, F32 expected) + static void require_near(const std::string& what, F32 actual, F32 expected) { - UNSCOPED_INFO(what + ": " + std::to_string(actual) + " vs " + std::to_string(expected)); + INFO(what); + CAPTURE(actual, expected); REQUIRE(fabsf(actual - expected) < 1e-3f); } }; @@ -139,20 +140,20 @@ namespace }); UNSCOPED_INFO("unrotated, one quad"); REQUIRE(flat.count == (size_t)6); - ensureNear("unrotated left", flat.min_x, 10.f); - ensureNear("unrotated right", flat.max_x, 19.f); - ensureNear("unrotated bottom", flat.min_y, 20.f); - ensureNear("unrotated top", flat.max_y, 29.f); + require_near("unrotated left", flat.min_x, 10.f); + require_near("unrotated right", flat.max_x, 19.f); + require_near("unrotated bottom", flat.min_y, 20.f); + require_near("unrotated top", flat.max_y, 29.f); const Bounds turned = boundsOf([&]() { gl_draw_scaled_rotated_image(10, 20, 9, 9, 90.f, texture.get()); }); UNSCOPED_INFO("turned, one quad"); REQUIRE(turned.count == (size_t)6); - ensureNear("turned left", turned.min_x, flat.min_x); - ensureNear("turned right", turned.max_x, flat.max_x); - ensureNear("turned bottom", turned.min_y, flat.min_y); - ensureNear("turned top", turned.max_y, flat.max_y); + require_near("turned left", turned.min_x, flat.min_x); + require_near("turned right", turned.max_x, flat.max_x); + require_near("turned bottom", turned.min_y, flat.min_y); + require_near("turned top", turned.max_y, flat.max_y); } // The turn is a turn: at a quarter turn a wide image stands tall, and @@ -167,11 +168,12 @@ namespace UNSCOPED_INFO("one quad"); REQUIRE(turned.count == (size_t)6); // Centre (14, 21); half extents swap to (1, 4). - ensureNear("left", turned.min_x, 13.f); - ensureNear("right", turned.max_x, 15.f); - ensureNear("bottom", turned.min_y, 17.f); - ensureNear("top", turned.max_y, 25.f); + require_near("left", turned.min_x, 13.f); + require_near("right", turned.max_x, 15.f); + require_near("bottom", turned.min_y, 17.f); + require_near("top", turned.max_y, 25.f); } + // Rectangles added to one batch are one draw, where gl_rect_2d makes // a draw of each; and they are the same rectangles. TEST_CASE_METHOD(llrender2dutils_data, "llrender2dutils_gl: gl_rect_2d_in_batch draws the same rectangles as gl_rect_2d in one draw", "[llrender2dutils_gl]") @@ -218,10 +220,10 @@ namespace gl_rect_2d_in_batch(40, 60, 50, 55, LLColor4::white); gGL.end(); }); - ensureNear("left", b.min_x, 10.f); - ensureNear("right", b.max_x, 50.f); - ensureNear("bottom", b.min_y, 25.f); - ensureNear("top", b.max_y, 60.f); + require_near("left", b.min_x, 10.f); + require_near("right", b.max_x, 50.f); + require_near("bottom", b.min_y, 25.f); + require_near("top", b.max_y, 60.f); } // A rect's outline covers the pixels just inside its edges, and @@ -260,8 +262,8 @@ namespace const U8 red = rgba[(static_cast(y) * W + x) * 4]; if ((red == 255) != edge) { - FAIL("at " + std::to_string(x) + "," + std::to_string(y) + " of the outline at " + std::to_string(rect.mLeft) + "," + - std::to_string(rect.mBottom) + ", scaled " + std::to_string(scale) + ": " + std::to_string(red)); + CAPTURE(rect.mLeft, rect.mBottom, scale, x, y, edge, static_cast(red)); + FAIL("a pixel off the outline is lit or one on it is not"); } } } diff --git a/indra/llrender/tests/llrendermatrix_test.cpp b/indra/llrender/tests/llrendermatrix_test.cpp index 1c0bd30035..5d79013bcf 100644 --- a/indra/llrender/tests/llrendermatrix_test.cpp +++ b/indra/llrender/tests/llrendermatrix_test.cpp @@ -36,7 +36,6 @@ #include #include -#include namespace { @@ -67,28 +66,26 @@ namespace }; // Every element within tolerance of the larger magnitude, or of one. - void ensure_matrix_close(const std::string& what, const LLMatrix4a& actual, const LLMatrix4a& expected, F32 tolerance) + void require_matrix_close(const std::string& what, const LLMatrix4a& actual, const LLMatrix4a& expected, F32 tolerance) { + INFO(what); const F32* a = actual.getF32ptr(); const F32* e = expected.getF32ptr(); for (S32 i = 0; i < 16; ++i) { const F32 scale = llmax(1.f, fabsf(a[i]), fabsf(e[i])); - std::ostringstream msg; - msg << what << " at element " << i << ": " << a[i] << " vs " << e[i]; - UNSCOPED_INFO(msg.str().c_str()); + CAPTURE(i); REQUIRE_THAT(a[i], Catch::Matchers::WithinAbs(e[i], tolerance * scale)); } } - void ensure_vector_close(const std::string& what, const LLVector4a& actual, const LLVector4a& expected, F32 tolerance) + void require_vector_close(const std::string& what, const LLVector4a& actual, const LLVector4a& expected, F32 tolerance) { + INFO(what); for (S32 i = 0; i < 3; ++i) { const F32 scale = llmax(1.f, fabsf(actual[i]), fabsf(expected[i])); - std::ostringstream msg; - msg << what << " at lane " << i << ": " << actual[i] << " vs " << expected[i]; - UNSCOPED_INFO(msg.str().c_str()); + CAPTURE(i); REQUIRE_THAT(actual[i], Catch::Matchers::WithinAbs(expected[i], tolerance * scale)); } } @@ -141,15 +138,14 @@ namespace expected *= s; expected *= t; expected *= LLMatrix4(M_ROWS); - ensure_matrix_close("the stack after translate, scale, rotate, mult", gGL.getModelviewMatrix(), LLMatrix4a(expected), 1e-5f); + require_matrix_close("the stack after translate, scale, rotate, mult", gGL.getModelviewMatrix(), LLMatrix4a(expected), 1e-5f); // push copies, pop restores gGL.pushMatrix(); gGL.loadIdentity(); - UNSCOPED_INFO("loadIdentity"); CHECK(gGL.getModelviewMatrix() == LLMatrix4a::identity()); gGL.popMatrix(); - ensure_matrix_close("popMatrix restores", gGL.getModelviewMatrix(), LLMatrix4a(expected), 1e-5f); + require_matrix_close("popMatrix restores", gGL.getModelviewMatrix(), LLMatrix4a(expected), 1e-5f); // the modes are separate stacks gGL.matrixMode(LLRender::MM_PROJECTION); @@ -157,24 +153,20 @@ namespace CHECK(gGL.getProjectionMatrix() == LLMatrix4a::identity()); } - // ortho on the stack and the projection helpers, in both depth conventions. TEST_CASE_METHOD(llrendermatrix_data, "llrendermatrix: the projection helpers and gGL.ortho under forward and reverse-Z", "[llrendermatrix]") { for (bool reverse : { false, true }) { LLRender::sReverseZ = reverse; - const std::string when = reverse ? " under reverse-Z" : " forward"; + CAPTURE(reverse); const LLMatrix4a ortho = LLMatrix4a::ortho(-3.f, 5.f, -2.f, 7.f, 0.5f, 100.f); const LLMatrix4a persp = LLMatrix4a::perspective(1.1f, 1.777f, 0.25f, 1024.f); const LLMatrix4a expected_ortho = reverse ? reversed_z(ortho) : ortho; const LLMatrix4a expected_persp = reverse ? reversed_z(persp) : persp; - UNSCOPED_INFO("al_ortho" + when); REQUIRE(same_bytes(al_ortho(-3.f, 5.f, -2.f, 7.f, 0.5f, 100.f), expected_ortho)); - UNSCOPED_INFO("al_perspective" + when); REQUIRE(same_bytes(al_perspective(1.1f, 1.777f, 0.25f, 1024.f), expected_persp)); - UNSCOPED_INFO("al_reverse_z_transform" + when); REQUIRE(same_bytes(al_reverse_z_transform(persp), reversed_z(persp))); // a reversed projection puts the near plane at window 1 and the @@ -182,9 +174,9 @@ namespace LLVector4a near_clip, far_clip; expected_persp.transform4(LLVector4a(0.f, 0.f, -0.25f, 1.f), near_clip); expected_persp.transform4(LLVector4a(0.f, 0.f, -1024.f, 1.f), far_clip); - UNSCOPED_INFO(("near plane depth" + when).c_str()); + UNSCOPED_INFO("near plane depth"); REQUIRE_THAT(near_clip[2] / near_clip[3], Catch::Matchers::WithinAbs(reverse ? 1.f : -1.f, 1e-5f)); - UNSCOPED_INFO(("far plane depth" + when).c_str()); + UNSCOPED_INFO("far plane depth"); REQUIRE_THAT(far_clip[2] / far_clip[3], Catch::Matchers::WithinAbs(reverse ? 0.f : 1.f, 1e-5f)); gGL.matrixMode(LLRender::MM_PROJECTION); @@ -192,7 +184,7 @@ namespace gGL.ortho(-3.f, 5.f, -2.f, 7.f, 0.5f, 100.f); LLMatrix4a expected_stack; expected_stack.setMul(expected_ortho, LLMatrix4a(M_ROWS)); - ensure_matrix_close("gGL.ortho applies ahead of the stack" + when, gGL.getProjectionMatrix(), expected_stack, 1e-5f); + require_matrix_close("gGL.ortho applies ahead of the stack", gGL.getProjectionMatrix(), expected_stack, 1e-5f); gGL.loadIdentity(); gGL.matrixMode(LLRender::MM_MODELVIEW); @@ -203,20 +195,17 @@ namespace const LLVector4a win = al_project(obj, mv, expected_persp, viewport); const LLVector4a expected_win = reverse ? alprojection::project_zo(obj, mv, expected_persp, viewport) : alprojection::project(obj, mv, expected_persp, viewport); - ensure_vector_close("al_project" + when, win, expected_win, 1e-6f); - { - INFO("window depth is in [0, 1]" + when); - REQUIRE(win[2] >= 0.f); - REQUIRE(win[2] <= 1.f); - } + require_vector_close("al_project", win, expected_win, 1e-6f); + REQUIRE(win[2] >= 0.f); + REQUIRE(win[2] <= 1.f); const LLVector4a back = al_unproject(win, mv, expected_persp, viewport); - ensure_vector_close("al_unproject of al_project" + when, back, obj, 5e-4f); + require_vector_close("al_unproject of al_project", back, obj, 5e-4f); LLMatrix4a inverse; inverse.setMul(mv, expected_persp); inverse.invert(); - ensure_vector_close("al_unproject through the inverse" + when, al_unproject(win, inverse, viewport), back, 1e-6f); + require_vector_close("al_unproject through the inverse", al_unproject(win, inverse, viewport), back, 1e-6f); } } @@ -254,13 +243,14 @@ namespace suffix.setColumn<2>(cplane); LLMatrix4a expected; expected.setMul(persp, suffix); - ensure_matrix_close("oblique projection", gGL.getProjectionMatrix(), expected, 1e-5f); + require_matrix_close("oblique projection", gGL.getProjectionMatrix(), expected, 1e-5f); // only the depth column moved for (S32 column : { 0, 1, 3 }) { for (S32 row = 0; row < 4; ++row) { + CAPTURE(column, row); UNSCOPED_INFO("the other columns are the projection's"); REQUIRE(gGL.getProjectionMatrix().mMatrix[row][column] == persp.mMatrix[row][column]); } diff --git a/indra/llrender/tests/llrendertarget_gl_test.cpp b/indra/llrender/tests/llrendertarget_gl_test.cpp index e077ac1405..7190efe840 100644 --- a/indra/llrender/tests/llrendertarget_gl_test.cpp +++ b/indra/llrender/tests/llrendertarget_gl_test.cpp @@ -100,13 +100,13 @@ namespace // Every texel of `texels` is `expected`. template - void ensureEvery(const std::string& what, const std::vector& texels, const T (&expected)[4]) + void require_every(const std::string& what, const std::vector& texels, const T (&expected)[4]) { + INFO(what); for (size_t i = 0; i < texels.size(); ++i) { - UNSCOPED_INFO(what + ", texel " + std::to_string(i / 4) + " channel " + std::to_string(i % 4)); - REQUIRE(texels[i] == - expected[i % 4]); + INFO("texel " << i / 4 << " channel " << i % 4); + REQUIRE(texels[i] == expected[i % 4]); } } @@ -142,7 +142,7 @@ namespace } // The integer program, linked. - GLuint linkInteger() + GLuint require_integer_program() { const GLuint vs = ll_test::compileTestShader(GL_VERTEX_SHADER, kVertex); const GLuint fs = ll_test::compileTestShader(GL_FRAGMENT_SHADER, kIntegerFragment); @@ -159,7 +159,6 @@ namespace glDeleteShader(fs); GLint linked = GL_FALSE; glGetProgramiv(program, GL_LINK_STATUS, &linked); - UNSCOPED_INFO("integer program links"); REQUIRE(linked == GL_TRUE); return program; } @@ -181,11 +180,9 @@ namespace glDeleteShader(fs); GLint linked = GL_FALSE; glGetProgramiv(mProgram, GL_LINK_STATUS, &linked); - UNSCOPED_INFO("program links"); REQUIRE(linked == GL_TRUE); glGenVertexArrays(1, &mVAO); - UNSCOPED_INFO("target allocates"); REQUIRE(mTarget.allocate(SIZE, SIZE, GL_RGBA8)); } @@ -234,12 +231,11 @@ namespace CHECK(drawn(-1) == OUTPUT_COLOURS[0]); for (S32 output = 0; output < 4; ++output) { - UNSCOPED_INFO("output " + std::to_string(output)); + CAPTURE(output); REQUIRE(drawn(output) == OUTPUT_COLOURS[output]); } UNSCOPED_INFO("rebound: output 0 again"); CHECK(drawn(-1) == OUTPUT_COLOURS[0]); - UNSCOPED_INFO("no GL error"); CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } @@ -248,46 +244,42 @@ namespace // alone when only the depth is asked for; a program's integer outputs reach them exactly. TEST_CASE_METHOD(llrendertarget_data, "llrendertarget_gl: integer attachments keep their formats through clear and draw", "[llrendertarget_gl]") { - mIntegerProgram = linkInteger(); + mIntegerProgram = require_integer_program(); glGenVertexArrays(1, &mVAO); - UNSCOPED_INFO("R32UI target allocates"); - CHECK(mInteger.allocate(INT_W, INT_H, GL_R32UI, /*depth=*/true)); - UNSCOPED_INFO("RG16I attaches"); + REQUIRE(mInteger.allocate(INT_W, INT_H, GL_R32UI, /*depth=*/true)); CHECK(mInteger.addColorAttachment(GL_RG16I)); - UNSCOPED_INFO("RGBA32UI attaches"); CHECK(mInteger.addColorAttachment(GL_RGBA32UI)); const GLenum formats[] = { GL_R32UI, GL_RG16I, GL_RGBA32UI }; for (U32 attachment = 0; attachment < 3; ++attachment) { - UNSCOPED_INFO("attachment " + std::to_string(attachment) + " recorded"); - REQUIRE(mInteger.getInternalFormat(attachment) == - (U32)formats[attachment]); + CAPTURE(attachment); + UNSCOPED_INFO("the target's record"); + REQUIRE(mInteger.getInternalFormat(attachment) == (U32)formats[attachment]); gGL.getTextureSlot(0)->bindManual(ALTextureSlot::TT_TEXTURE, mInteger.getTexture(attachment)); GLint format = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &format); - UNSCOPED_INFO("attachment " + std::to_string(attachment) + " storage"); + UNSCOPED_INFO("the texture's storage"); REQUIRE((U32)format == (U32)formats[attachment]); } gGL.getTextureSlot(0)->unbind(); mInteger.bindTarget(); - UNSCOPED_INFO("complete"); CHECK((U32)glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == (U32)GL_FRAMEBUFFER_COMPLETE); gGL.setClearColor(LLColor4(7.f, 9.f, 11.f, 13.f)); mInteger.clear(); - ensureEvery("R32UI cleared", readInteger(0), { 7u, 0u, 0u, 1u }); - ensureEvery("RG16I cleared", readInteger(1), { 7, 9, 0, 1 }); - ensureEvery("RGBA32UI cleared", readInteger(2), { 7u, 9u, 11u, 13u }); + require_every("R32UI cleared", readInteger(0), { 7u, 0u, 0u, 1u }); + require_every("RG16I cleared", readInteger(1), { 7, 9, 0, 1 }); + require_every("RGBA32UI cleared", readInteger(2), { 7u, 9u, 11u, 13u }); gGL.setClearColor(LLColor4(-5.4f, -6.6f, 1.e10f, 0.4f)); mInteger.clear(GL_DEPTH_BUFFER_BIT); - ensureEvery("R32UI left alone by a depth clear", readInteger(0), { 7u, 0u, 0u, 1u }); + require_every("R32UI left alone by a depth clear", readInteger(0), { 7u, 0u, 0u, 1u }); mInteger.clear(); - ensureEvery("R32UI held at 0", readInteger(0), { 0u, 0u, 0u, 1u }); - ensureEvery("RG16I below 0", readInteger(1), { -5, -7, 0, 1 }); - ensureEvery("RGBA32UI held at its top", readInteger(2), { 0u, 0u, 0xFFFFFFFFu, 0u }); + require_every("R32UI held at 0", readInteger(0), { 0u, 0u, 0u, 1u }); + require_every("RG16I below 0", readInteger(1), { -5, -7, 0, 1 }); + require_every("RGBA32UI held at its top", readInteger(2), { 0u, 0u, 0xFFFFFFFFu, 0u }); LLGLDisable depth_test(GL_DEPTH_TEST); glUseProgram(mIntegerProgram); @@ -304,25 +296,24 @@ namespace { for (U32 x = 0; x < INT_W; ++x) { - const std::string at = " at " + std::to_string(x) + "," + std::to_string(y); + CAPTURE(x, y); const size_t i = ((size_t)y * INT_W + x) * 4; - UNSCOPED_INFO("R32UI drawn" + at); + UNSCOPED_INFO("R32UI drawn"); REQUIRE(uints[i] == 0x01000001u + y * 1000u + x); - UNSCOPED_INFO("RG16I r drawn" + at); + UNSCOPED_INFO("RG16I r drawn"); REQUIRE(ints[i] == -1 - (S32)x); - UNSCOPED_INFO("RG16I g drawn" + at); + UNSCOPED_INFO("RG16I g drawn"); REQUIRE(ints[i + 1] == (S32)y * 300 - 1000); - UNSCOPED_INFO("RGBA32UI r drawn" + at); + UNSCOPED_INFO("RGBA32UI r drawn"); REQUIRE(wide[i] == x); - UNSCOPED_INFO("RGBA32UI g drawn" + at); + UNSCOPED_INFO("RGBA32UI g drawn"); REQUIRE(wide[i + 1] == y); - UNSCOPED_INFO("RGBA32UI b drawn" + at); + UNSCOPED_INFO("RGBA32UI b drawn"); REQUIRE(wide[i + 2] == 0xFFFFFFFFu - x); - UNSCOPED_INFO("RGBA32UI a drawn" + at); + UNSCOPED_INFO("RGBA32UI a drawn"); REQUIRE(wide[i + 3] == 0x80000000u + y); } } - UNSCOPED_INFO("no GL error"); CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } @@ -330,9 +321,7 @@ namespace // and with the draw buffers narrowed to the first it clears that one alone, as it would a target of one kind. TEST_CASE_METHOD(llrendertarget_data, "llrendertarget_gl: a mixed float and integer target clears each attachment as its kind", "[llrendertarget_gl]") { - UNSCOPED_INFO("RGBA8 target allocates"); - CHECK(mMixed.allocate(INT_W, INT_H, GL_RGBA8)); - UNSCOPED_INFO("R32UI attaches"); + REQUIRE(mMixed.allocate(INT_W, INT_H, GL_RGBA8)); CHECK(mMixed.addColorAttachment(GL_R32UI)); mMixed.bindTarget(); @@ -341,18 +330,17 @@ namespace std::vector rgba((size_t)INT_W * INT_H * 4, 0x55); glReadBuffer(GL_COLOR_ATTACHMENT0); glReadPixels(0, 0, INT_W, INT_H, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data()); - ensureEvery("RGBA8 cleared", rgba, { 255, 0, 255, 255 }); - ensureEvery("R32UI cleared", readInteger(1), { 1u, 0u, 0u, 1u }); + require_every("RGBA8 cleared", rgba, { 255, 0, 255, 255 }); + require_every("R32UI cleared", readInteger(1), { 1u, 0u, 0u, 1u }); mMixed.setDrawBuffers(1); gGL.setClearColor(LLColor4::transparent); mMixed.clear(); glReadPixels(0, 0, INT_W, INT_H, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data()); - ensureEvery("RGBA8 cleared again", rgba, { 0, 0, 0, 0 }); - ensureEvery("R32UI not drawn to, so left", readInteger(1), { 1u, 0u, 0u, 1u }); + require_every("RGBA8 cleared again", rgba, { 0, 0, 0, 0 }); + require_every("R32UI not drawn to, so left", readInteger(1), { 1u, 0u, 0u, 1u }); mMixed.flush(); - UNSCOPED_INFO("no GL error"); CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } @@ -363,24 +351,20 @@ namespace const std::vector zeros((size_t)INT_W * INT_H, 0u); mLentImage = new LLImageGL(INT_W, INT_H, 1, /*usemipmaps=*/false); mLentImage->setExplicitFormat(GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT); - UNSCOPED_INFO("R32UI image created"); CHECK(mLentImage->createGLTexture(0, reinterpret_cast(zeros.data()))); LLRenderTarget::sUseFBO = true; mLent.setColorAttachment(mLentImage.get()); - UNSCOPED_INFO("the lent texture's format"); CHECK(mLent.getInternalFormat(0) == (U32)GL_R32UI); mLent.bindTarget(); gGL.setClearColor(LLColor4(5.f, 6.f, 7.f, 8.f)); mLent.clear(); - ensureEvery("R32UI cleared to its integers", readInteger(0), { 5u, 0u, 0u, 1u }); + require_every("R32UI cleared to its integers", readInteger(0), { 5u, 0u, 0u, 1u }); mLent.flush(); mLent.releaseColorAttachment(); - UNSCOPED_INFO("returned, its format forgotten"); CHECK(mLent.getInternalFormat(0) == 0u); - UNSCOPED_INFO("no GL error"); CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } } diff --git a/indra/llrender/tests/lluiimage_test.cpp b/indra/llrender/tests/lluiimage_test.cpp index 99ab849071..bc6e8210d7 100644 --- a/indra/llrender/tests/lluiimage_test.cpp +++ b/indra/llrender/tests/lluiimage_test.cpp @@ -121,9 +121,8 @@ namespace } // A recording's bounds are the image's size, from the origin. - static void ensureExtent(const std::vector& verts, F32 width, F32 height) + static void require_extent(const std::vector& verts, F32 width, F32 height) { - UNSCOPED_INFO("the recording holds vertices"); REQUIRE_FALSE(verts.empty()); F32 min_x = verts[0].mV[VX]; F32 max_x = min_x; @@ -136,13 +135,9 @@ namespace min_y = llmin(min_y, v.mV[VY]); max_y = llmax(max_y, v.mV[VY]); } - UNSCOPED_INFO("starts at the origin in x"); REQUIRE(min_x == 0.f); - UNSCOPED_INFO("starts at the origin in y"); REQUIRE(min_y == 0.f); - UNSCOPED_INFO("spans the width"); REQUIRE(max_x == width); - UNSCOPED_INFO("spans the height"); REQUIRE(max_y == height); } }; @@ -195,7 +190,7 @@ namespace const std::vector verts = ll_test::UIImageProbe::positions(*image); UNSCOPED_INFO("a plain image is one quad"); REQUIRE(verts.size() == (size_t)6); - ensureExtent(verts, 8.f, 6.f); + require_extent(verts, 8.f, 6.f); } // The nine-slice path works out its own placement from the UI offset, so @@ -214,6 +209,6 @@ namespace const std::vector verts = ll_test::UIImageProbe::positions(*image); UNSCOPED_INFO("nine slices are eighteen triangles"); REQUIRE(verts.size() == (size_t)54); - ensureExtent(verts, 32.f, 24.f); + require_extent(verts, 32.f, 24.f); } } diff --git a/indra/llrender/tests/lluitransform_test.cpp b/indra/llrender/tests/lluitransform_test.cpp index fc12fe4bb1..56c4604d77 100644 --- a/indra/llrender/tests/lluitransform_test.cpp +++ b/indra/llrender/tests/lluitransform_test.cpp @@ -60,12 +60,10 @@ namespace }; - // With nothing pushed, a local rect is where it says it is. TEST_CASE_METHOD(lluitransform_data, "lluitransform: with nothing pushed a local rect is where it says it is", "[lluitransform]") { const LLRect local(10, 40, 110, 20); const LLRect screen = LLRender2D::toScreen(local); - UNSCOPED_INFO("untransformed, it is itself " + where(screen)); CHECK(where(screen) == where(local)); } @@ -74,13 +72,9 @@ namespace { LLFontGL::sCurOrigin.set(7, 13); const LLRect screen = LLRender2D::toScreen(LLRect(10, 40, 110, 20)); - UNSCOPED_INFO("moved by the offset"); CHECK(screen.mLeft == 17); - UNSCOPED_INFO("in both directions"); CHECK(screen.mBottom == 33); - UNSCOPED_INFO("keeping its width"); CHECK(screen.getWidth() == 100); - UNSCOPED_INFO("and its height"); CHECK(screen.getHeight() == 20); } @@ -96,16 +90,18 @@ namespace LLFontGL::sCurScaleY = 2.f; const LLRect screen = LLRender2D::toScreen(LLRect(0, 50, 100, 0)); - UNSCOPED_INFO("the near edge is the scaled offset"); - CHECK(screen.mLeft == 20); - UNSCOPED_INFO("and so is the bottom"); - CHECK(screen.mBottom == 40); + { + INFO("the near edges are the scaled offset"); + CHECK(screen.mLeft == 20); + CHECK(screen.mBottom == 40); + } UNSCOPED_INFO("the far edge is scaled too"); CHECK(screen.mRight == 220); - UNSCOPED_INFO("the rect is twice the size"); - CHECK(screen.getWidth() == 200); - UNSCOPED_INFO("in both directions"); - CHECK(screen.getHeight() == 100); + { + INFO("the rect is twice the size"); + CHECK(screen.getWidth() == 200); + CHECK(screen.getHeight() == 100); + } } // The two axes are independent, because the renderer's are. @@ -115,7 +111,6 @@ namespace LLFontGL::sCurScaleY = 1.f; const LLRect screen = LLRender2D::toScreen(LLRect(0, 10, 10, 0)); - UNSCOPED_INFO("across"); CHECK(screen.getWidth() == 30); UNSCOPED_INFO("down is left alone"); CHECK(screen.getHeight() == 10); @@ -131,16 +126,15 @@ namespace F32 x = 0.f; F32 y = 0.f; LLRender2D::toScreen(20.f, 30.f, x, y); - UNSCOPED_INFO("across"); CHECK((S32)ll_round(x) == 36); - UNSCOPED_INFO("down"); CHECK((S32)ll_round(y) == 90); const LLRect screen = LLRender2D::toScreen(LLRect(20, 30, 20, 30)); - UNSCOPED_INFO("the rect's corner is the same point"); - CHECK(screen.mLeft == 36); - UNSCOPED_INFO("and the same on the other axis"); - CHECK(screen.mBottom == 90); + { + INFO("the rect's corner is the same point"); + CHECK(screen.mLeft == 36); + CHECK(screen.mBottom == 90); + } } // A scale keeps the origin it was pushed at: what is under it is drawn @@ -155,16 +149,18 @@ namespace F32 x = 0.f; F32 y = 0.f; LLRender2D::toScreen(0.f, 0.f, x, y); - UNSCOPED_INFO("the corner it was pushed at is where it was"); - CHECK(ll_round(x) == 300); - UNSCOPED_INFO("on both axes"); - CHECK(ll_round(y) == 200); + { + INFO("the corner it was pushed at is where it was"); + CHECK(ll_round(x) == 300); + CHECK(ll_round(y) == 200); + } LLRender2D::toScreen(50.f, 40.f, x, y); - UNSCOPED_INFO("and what is on it is twice as far along"); - CHECK(ll_round(x) == 400); - UNSCOPED_INFO("in both directions"); - CHECK(ll_round(y) == 280); + { + INFO("what is on it is twice as far along"); + CHECK(ll_round(x) == 400); + CHECK(ll_round(y) == 280); + } LLRender2D::popMatrix(); } @@ -182,11 +178,12 @@ namespace F32 x = 0.f; F32 y = 0.f; LLRender2D::toScreen(10.f, 10.f, x, y); - UNSCOPED_INFO("the offset is the one the frame was pushed on"); - CHECK(ll_round(x) == 40); - UNSCOPED_INFO("on both axes"); - CHECK(ll_round(y) == 50); - UNSCOPED_INFO("and the scale is off with it"); + { + INFO("the offset is the one the frame was pushed on"); + CHECK(ll_round(x) == 40); + CHECK(ll_round(y) == 50); + } + UNSCOPED_INFO("the scale is off with it"); CHECK(LLFontGL::sCurScaleX == 1.f); LLRender2D::popMatrix(); From 4ca70e11b9a382b31c3ebfa7c4b2c21043f86e47 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 13 Oct 2026 07:28:50 -0400 Subject: [PATCH 3/4] LLFontGL's explicit-path initClass, called again, changes nothing The overload that reads fonts.xml from a path, which only the tests call, is single-shot: with a registry already made it warns and returns. But it set the DPI, the scale and the application directory first, so a second call with other values left the fonts already made at one DPI and every later layout and measurement reading another. It now returns before setting anything. llfontgl's "a second initClass leaves the fonts already resolved in place" calls it again at 72 DPI and half scale, and left those for whatever ran next. In its own file that was the next case, whose own call at 96 put them back. In one process with llfonttextcache, an ellipsized render 40 pixels wide could follow it instead, and drew no character before the dots, in three of ten random orders. A new llfontgl case holds the DPI and scale to what they were, and fails without the change. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/llfontgl.cpp | 16 +++++++++------- indra/llrender/tests/llfontgl_test.cpp | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/indra/llrender/llfontgl.cpp b/indra/llrender/llfontgl.cpp index f3ae317f7f..d7d269ec1e 100644 --- a/indra/llrender/llfontgl.cpp +++ b/indra/llrender/llfontgl.cpp @@ -1965,21 +1965,23 @@ void LLFontGL::initClass(F32 screen_dpi, F32 x_scale, F32 y_scale, bool create_gl_textures) { LL_PROFILE_ZONE_SCOPED_CATEGORY_UI; - sVertDPI = (F32)llfloor(screen_dpi * y_scale); - sHorizDPI = (F32)llfloor(screen_dpi * x_scale); - sScaleX = x_scale; - sScaleY = y_scale; - sAppDir = app_dir; - // Single-shot: bail if the registry was already created via either // overload. Reload-with-explicit-path isn't supported — the test - // caller initializes once. + // caller initializes once. Ignored whole: the fonts already made were + // made at the DPI and scale they have, which a second call must not + // change under them. if (sFontRegistry) { LL_WARNS() << "LLFontGL::initClass(explicit path) called with an existing registry; ignoring" << LL_ENDL; return; } + sVertDPI = (F32)llfloor(screen_dpi * y_scale); + sHorizDPI = (F32)llfloor(screen_dpi * x_scale); + sScaleX = x_scale; + sScaleY = y_scale; + sAppDir = app_dir; + sFontRegistry = new LLFontRegistry(create_gl_textures); sFontRegistry->parseFontInfoFromFile(fonts_xml_path, font_overrides); diff --git a/indra/llrender/tests/llfontgl_test.cpp b/indra/llrender/tests/llfontgl_test.cpp index bbf15b1af1..044359ca6f 100644 --- a/indra/llrender/tests/llfontgl_test.cpp +++ b/indra/llrender/tests/llfontgl_test.cpp @@ -313,6 +313,28 @@ namespace CHECK(after == before); } + // A second initClass through the explicit-path overload is ignored + // whole: the fonts already made keep the DPI and scale they were made + // at, which every later layout and measurement reads. + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: a second initClass leaves the DPI and scale the fonts were made at", "[LLFontGL]") + { + if (!fileExists(kFontsXml)) + SKIP("fonts.xml not found"); + LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, + LLSD(), /*create_gl_textures=*/true); + const F32 horiz_dpi = LLFontGL::sHorizDPI; + const F32 vert_dpi = LLFontGL::sVertDPI; + const F32 scale_x = LLFontGL::sScaleX; + const F32 scale_y = LLFontGL::sScaleY; + + LLFontGL::initClass(72.f, 0.5f, 0.5f, kAppDir, kFontsXml, + LLSD(), /*create_gl_textures=*/true); + CHECK(LLFontGL::sHorizDPI == horiz_dpi); + CHECK(LLFontGL::sVertDPI == vert_dpi); + CHECK(LLFontGL::sScaleX == scale_x); + CHECK(LLFontGL::sScaleY == scale_y); + } + // Static getter family — every named getter must resolve to a // non-null LLFontGL after a successful initClass. Smoke check // that the default fonts.xml covers all the families exposed. From 5b26ba4a5836de7c766bd1f52d116db2d5cc7f66 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 13 Oct 2026 07:48:19 -0400 Subject: [PATCH 4/4] The state guard watches what llrender keeps for the process, and the shared GL context ends with the main llrender_test_checks, a listener in each of llrender's three binaries, gives the state guard its checks after every case: - the GL context current on the test's thread: a case that makes one ends it, and one on the shared context leaves that one current; - how deep each of gGL's matrix stacks is, and which one its calls go to: a push left behind stays for every later case, and the stacks are 32 deep. LLRender keeps its stacks private, so it friends ll_test::RenderProbe, as lluiimage.h and llfonttextcache.h friend theirs; - the bound shader, reverse-Z and compute, and the window's viewport; - GL errors left unread, which the next case's own check for none would find. Reading them clears them, so once it has reported, the guard reads what is left, and the case after is not blamed for clearing them; - GL images alive with no context current: in the fresh binary, an image that outlives its case holds a name in a context that is gone. While the shared context lives, the font atlas pages rasterized into it outlive the cases that filled them, by design; - the font manager, LLFontGL's DPI and scale, which a later case's text is laid out and measured at, and its UI origin, depth, scale and pushed transforms. Each binary takes llcommon's checks too. I broke a case each way, and each check named it. The DPI and scale check names llfontgl's second initClass case without the change before this one. In a Catch2 binary, sharedHeadlessGL() and installWhiteTexture() hand their ends to the main's teardown, which runs after the last case while APR and LLError are still up, not to static destruction; the white texture goes first, before the context it was made in. Making the shared context changes the context, the bound program, the pushed matrices and whatever the driver chooses for the process, so the guard takes what it leaves as the state to compare with, rather than blaming the case that first asked. The fixture header is llui's and newview's TUT tests' too, so they keep the function-local statics they had. Each binary, run whole, passes 10 random orders and 20 repeats with no guard report. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/CMakeLists.txt | 9 + indra/llrender/llrender.h | 4 + indra/llrender/tests/llheadlessgl_fixture.h | 31 +++- indra/llrender/tests/llrender_test_checks.cpp | 158 ++++++++++++++++++ 4 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 indra/llrender/tests/llrender_test_checks.cpp diff --git a/indra/llrender/CMakeLists.txt b/indra/llrender/CMakeLists.txt index aab0d636d2..ab6c6cb3af 100644 --- a/indra/llrender/CMakeLists.txt +++ b/indra/llrender/CMakeLists.txt @@ -133,6 +133,12 @@ if(AL_BUILD_TESTS) "LLFONT_TEST_FONTS_XML=\"${font_test_fonts_xml}\"" ) + # The state guard's checks on what llrender keeps for the process, for + # each of its test binaries. + add_library(llrender_test_checks OBJECT tests/llrender_test_checks.cpp) + target_link_libraries(llrender_test_checks PRIVATE al::flags al_test_support llrender) + set_target_properties(llrender_test_checks PROPERTIES FOLDER "Tests/llrender") + # The suites that need no GL: arithmetic (where a local coordinate lands # under the UI transform; the matrix stack, the projection helpers and the # clip-plane classes), and the font tables and shaping. alfontshaping holds @@ -150,6 +156,7 @@ if(AL_BUILD_TESTS) tests/llrendermatrix_test.cpp tests/lluitransform_test.cpp LIBRARIES llrender ll::icu + SUPPORT llrender_test_checks llcommon_test_checks DEFINES ${font_defines} ) @@ -169,6 +176,7 @@ if(AL_BUILD_TESTS) tests/llfontregistry_gl_test.cpp tests/llimagegl_test.cpp LIBRARIES llrender ll::icu + SUPPORT llrender_test_checks llcommon_test_checks DEFINES ${font_defines} GL ) @@ -213,6 +221,7 @@ if(AL_BUILD_TESTS) tests/llrendertarget_gl_test.cpp tests/lluiimage_test.cpp LIBRARIES llrender + SUPPORT llrender_test_checks llcommon_test_checks DEFINES ${font_defines} GL ) diff --git a/indra/llrender/llrender.h b/indra/llrender/llrender.h index e9f8c3a06b..cc9e87b226 100644 --- a/indra/llrender/llrender.h +++ b/indra/llrender/llrender.h @@ -62,6 +62,8 @@ class LLRenderTarget; class LLTexture; class LLVertexBufferData; +namespace ll_test { struct RenderProbe; } + #define LL_MATRIX_STACK_DEPTH 32 // AL_NUM_TEXTURE_SLOTS lives in altextureslot.h alongside the units it sizes. @@ -565,6 +567,8 @@ class LLRender private: friend class LLLightState; + // The tests' state guard reads how deep each matrix stack is. + friend struct ll_test::RenderProbe; LLVertexBuffer* bufferfromCache(U32 attribute_mask, U32 count); LLVertexBuffer* genBuffer(U32 attribute_mask, S32 count); diff --git a/indra/llrender/tests/llheadlessgl_fixture.h b/indra/llrender/tests/llheadlessgl_fixture.h index 4069eb5ed4..52f9771560 100644 --- a/indra/llrender/tests/llheadlessgl_fixture.h +++ b/indra/llrender/tests/llheadlessgl_fixture.h @@ -56,6 +56,11 @@ #include "llfile.h" #include "lluuid.h" +#if AL_TEST_CATCH2 +#include "alteststateguard.h" +#include "altestteardown.h" +#endif + #include #include #include @@ -320,7 +325,8 @@ namespace ll_test // A white texel where the viewer puts its white image, which an unbound // texture slot samples: what is drawn with no texture then comes out in // its own colour, as it does in the viewer, rather than black. Made once, - // on the first call; the context must be up. + // on the first call; the context must be up. A Catch2 binary releases it + // after the last case, before the shared context it was made in ends. inline void installWhiteTexture() { static LLPointer white; @@ -329,6 +335,9 @@ namespace ll_test LLPointer raw = new LLImageRaw(1, 1, 4); std::memset(raw->getData(), 0xFF, 4); white = new LLImageGL(raw, false); +#if AL_TEST_CATCH2 + ll_test::Teardown::add([] { white = nullptr; }); +#endif } ALTextureSlot::sWhiteTexture = white->getTexName(); // Bound once, so that the slot holds a texture: an unbind puts the @@ -541,13 +550,33 @@ namespace ll_test // tests need, so each must leave the image, render and shader state as // it found it. Tests that make a HeadlessGL of their own per test end // the GL classes under it, and are never linked beside a caller of this. + // + // A Catch2 binary ends it after the last case, while APR and LLError are + // still up, rather than at static destruction. What making it changes -- + // the context, the bound program, the matrices pushed, and whatever the + // driver chooses for the process -- is the state the guard compares with + // from then on, so the case that first asked is not blamed for it. inline HeadlessGL& sharedHeadlessGL() { +#if AL_TEST_CATCH2 + static HeadlessGL* gl = [] + { + HeadlessGL* made = new HeadlessGL(/*needs_vbos=*/true, + /*needs_imagegl=*/true, + /*needs_llrender=*/true, + /*needs_render=*/true); + ll_test::Teardown::add([made] { delete made; }); + ll_test::StateGuard::accept(); + return made; + }(); + return *gl; +#else static HeadlessGL gl(/*needs_vbos=*/true, /*needs_imagegl=*/true, /*needs_llrender=*/true, /*needs_render=*/true); return gl; +#endif } // A test's hold on the shared context. It takes sharedHeadlessGL() as it diff --git a/indra/llrender/tests/llrender_test_checks.cpp b/indra/llrender/tests/llrender_test_checks.cpp new file mode 100644 index 0000000000..41a5e61cfb --- /dev/null +++ b/indra/llrender/tests/llrender_test_checks.cpp @@ -0,0 +1,158 @@ +/** + * @file llrender_test_checks.cpp + * @brief The state guard's checks on what llrender keeps for the process: the + * GL context, gGL's matrix stacks and switches, the bound shader, the + * live GL images, the font manager, and the DPI, scale and UI + * transform text is laid out at. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * 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; + * version 2.1 of the License only. + * + * 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 + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "../llfontfreetype.h" +#include "../llfontgl.h" +#include "../llglslshader.h" +#include "../llimagegl.h" +#include "../llrender.h" + +#include "alteststateguard.h" + +#include "SDL3/SDL_video.h" + +#include +#include + +#include + +namespace ll_test +{ + // Friended in llrender.h, which keeps its stacks to itself. + struct RenderProbe + { + // How many matrices each of gGL's stacks has pushed, and which stack + // its matrix calls go to. + static std::string matrixStacks() + { + static const char* const names[LLRender::NUM_MATRIX_MODES] = { "modelview", "projection", "texture" }; + std::string text; + for (U32 mode = 0; mode < LLRender::NUM_MATRIX_MODES; ++mode) + { + text += std::string(names[mode]) + " " + std::to_string(gGL.mMatIdx[mode]) + ", "; + } + return text + "calls to " + names[gGL.mMatrixMode]; + } + }; +} + +namespace +{ + // The context current on this thread: its handle, or none. The hidden + // window's context is SDL's, on every platform. + std::string currentContext() + { + SDL_GLContext context = SDL_GL_GetCurrentContext(); + return context ? llformat("%p", static_cast(context)) : std::string("none"); + } + + // The errors GL holds for the next glGetError, which reading clears. + std::string unreadErrors() + { + if (!SDL_GL_GetCurrentContext()) + { + return "no context"; + } + std::string text; + // A context holds one flag per kind of error, so few can be pending. + for (S32 i = 0; i < 8; ++i) + { + const GLenum error = glGetError(); + if (error == GL_NO_ERROR) + { + break; + } + text += llformat("%s0x%04x", text.empty() ? "" : ", ", error); + } + return text.empty() ? std::string("none") : text; + } + + class LLRenderChecks : public Catch::EventListenerBase + { + public: + using Catch::EventListenerBase::EventListenerBase; + + void testRunStarting(Catch::TestRunInfo const&) override + { + // A case that makes a context of its own ends it, and one on the + // shared context leaves that one current. + ll_test::StateGuard::add("the GL context", currentContext); + // A case pops what it pushed: the stacks are LL_MATRIX_STACK_DEPTH + // deep, and a case after one that left a push draws through it. + ll_test::StateGuard::add("gGL's matrix stacks", ll_test::RenderProbe::matrixStacks); + ll_test::StateGuard::add("the bound shader", [] + { + const LLGLSLShader* shader = LLGLSLShader::sCurBoundShaderPtr; + return shader ? shader->mName : std::string("none"); + }); + ll_test::StateGuard::add("LLRender's switches", [] + { + return std::string("reverse-Z ") + (LLRender::sReverseZ ? "on" : "off") + + ", compute " + (LLRender::sCompute ? "on" : "off"); + }); + ll_test::StateGuard::add("the GL viewport", [] + { + return llformat("%d %d %d %d", gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3]); + }); + // A case reads the errors it made: left, the next case's own + // check for none would find them. Reading them clears them, so + // the guard reads what is left once it has reported, and the + // next case starts with none rather than being blamed for that. + ll_test::StateGuard::add("unread GL errors", unreadErrors, [] { unreadErrors(); }); + // A case that ends its context releases the images it made in it: + // one left over holds a name in a context that is gone. While a + // shared context lives, the images in it may outlive a case, as + // the font atlas pages it rasterizes into do. + ll_test::StateGuard::add("GL images outliving their context", [] + { + return SDL_GL_GetCurrentContext() ? std::string("a context is current") + : std::to_string(LLImageGL::sImageList.size()); + }); + ll_test::StateGuard::add("the font manager", + [] { return std::string(gFontManagerp ? "live" : "none"); }); + // The fonts are made for the process at one DPI and scale, which + // a later case's text is laid out and measured at. + ll_test::StateGuard::add("LLFontGL's DPI and scale", [] + { + return llformat("DPI %g x %g, scale %g x %g", LLFontGL::sHorizDPI, LLFontGL::sVertDPI, + LLFontGL::sScaleX, LLFontGL::sScaleY); + }); + // A case that moves the UI origin, or pushes a transform, puts it + // back: the next case's text and images land where it says. + ll_test::StateGuard::add("LLFontGL's UI transform", [] + { + return llformat("origin %g %g, depth %g, scale %g x %g, %zu pushed", LLFontGL::sCurOrigin.mX, + LLFontGL::sCurOrigin.mY, LLFontGL::sCurDepth, LLFontGL::sCurScaleX, + LLFontGL::sCurScaleY, LLFontGL::sOriginStack.size()); + }); + } + }; +} + +CATCH_REGISTER_LISTENER(LLRenderChecks)