/* * \brief Blitting test * \author Norman Feske * \date 2025-01-16 */ /* * Copyright (C) 2025 Genode Labs GmbH * * This file is part of the Genode OS framework, which is distributed * under the terms of the GNU Affero General Public License version 3. */ #include #include #include #include using namespace Blit; /******************************* ** Low-level SIMD operations ** *******************************/ template struct Image { static constexpr unsigned w = W, h = H; uint32_t pixels[W*H]; void print(Output &out) const { using Genode::print; for (unsigned y = 0; y < H; y++) { for (unsigned x = 0; x < min(25u, W); x++) { uint32_t v = pixels[y*W+x]; if (v) print(out, " ", Char('A' + (v&63)), Char(char('A' + ((v>>16)&63)))); else print(out, " ."); } if (y < H-1) print(out, "\n"); } } bool operator != (Image const &other) { for (unsigned i = 0; i < W*H; i++) if (other.pixels[i] != pixels[i]) return true; return false; } static Image pattern() { Image image { }; for (unsigned y = 0; y < H; y++) for (unsigned x = 0; x < W; x++) image.pixels[y*W + x] = (y << 16) | x; return image; } }; #define TEST_LANDSCAPE(SIMD, FN, DST_W, DST_H, W, H) \ { \ Image dst { }, ref { }; \ Slow:: FN(ref.pixels, ref.w/8, src.pixels, W, H); \ log(#FN, " ref:\n", ref); \ SIMD:: FN(dst.pixels, dst.w/8, src.pixels, W, H); \ log(#FN, " got:\n", dst); \ if (dst != ref) { \ error("", #FN, " failed"); \ throw 1; \ } \ } #define TEST_PORTRAIT(SIMD, FN, DST_W, DST_H, W, H) \ { \ Image dst { }, ref { }; \ Slow:: FN(ref.pixels, ref.w/8, src.pixels, src.w/8, W, H); \ log(#FN, " ref:\n", ref); \ SIMD:: FN(dst.pixels, dst.w/8, src.pixels, src.w/8, W, H); \ log(#FN, " got:\n", dst); \ if (dst != ref) { \ error("", #FN, " failed"); \ throw 1; \ } \ } template static void test_simd_b2f() { static Image<48,32> const src = Image<48,32>::pattern(); log("source image:\n", src); TEST_LANDSCAPE ( SIMD, B2f ::r0, 48, 32, 2, 4 ); TEST_LANDSCAPE ( SIMD, B2f_flip ::r0, 48, 32, 2, 4 ); TEST_PORTRAIT ( SIMD, B2f ::r90, 32, 48, 4, 2 ); TEST_PORTRAIT ( SIMD, B2f_flip ::r90, 32, 48, 4, 2 ); TEST_LANDSCAPE ( SIMD, B2f ::r180, 48, 32, 2, 4 ); TEST_LANDSCAPE ( SIMD, B2f_flip::r180, 48, 32, 2, 4 ); TEST_PORTRAIT ( SIMD, B2f ::r270, 32, 48, 4, 2 ); TEST_PORTRAIT ( SIMD, B2f_flip::r270, 32, 48, 4, 2 ); } /**************************************** ** Back-to-front argument dispatching ** ****************************************/ struct Recorded { struct Args { uint32_t *dst; unsigned dst_w; uint32_t const *src; unsigned src_w; unsigned w, h; bool operator != (Args const &other) const { return dst != other.dst || dst_w != other.dst_w || src != other.src || src_w != other.src_w || w != other.w || h != other.h; } void print(Output &out) const { bool const valid = (*this != Args { }); if (!valid) { Genode::print(out, "invalid"); return; } /* print src and dst pointer values in units of uint32_t words */ Genode::print(out, "dst=", Hex(addr_t(dst)/4), " dst_w=", dst_w, " src=", Hex(addr_t(src)/4), " src_w=", src_w, " w=", w, " h=", h); } }; static Args recorded; static void _record(uint32_t *dst, unsigned line_w, uint32_t const *src, unsigned w, unsigned h) { recorded = { dst, line_w, src, line_w, w, h }; } static void _record(uint32_t *dst, unsigned dst_w, uint32_t const *src, unsigned src_w, unsigned w, unsigned h) { recorded = { dst, dst_w, src, src_w, w, h }; } struct B2f { static inline void r0 (auto &&... args) { _record(args...); } static inline void r90 (auto &&... args) { _record(args...); } static inline void r180 (auto &&... args) { _record(args...); } static inline void r270 (auto &&... args) { _record(args...); } }; struct B2f_flip { static inline void r0 (auto &&... args) { _record(args...); } static inline void r90 (auto &&... args) { _record(args...); } static inline void r180(auto &&... args) { _record(args...); } static inline void r270(auto &&... args) { _record(args...); } }; }; Recorded::Args Recorded::recorded { }; namespace Blit { static inline const char *name(Rotate r) { switch (r) { case Rotate::R0: return "R0"; case Rotate::R90: return "R90"; case Rotate::R180: return "R180"; case Rotate::R270: return "R270"; } return "invalid"; } } static inline void test_b2f_dispatch() { Texture texture_landscape { nullptr, nullptr, { 640, 480 } }; Texture texture_portrait { nullptr, nullptr, { 480, 640 } }; Surface surface { nullptr, { 640, 480 } }; struct Expected : Recorded::Args { }; auto expected = [&] (addr_t dst, unsigned dst_w, addr_t src, unsigned src_w, unsigned w, unsigned h) { return Expected { (uint32_t *)(4*dst), dst_w, (uint32_t *)(4*src), src_w, w, h }; }; using Rect = Blit::Rect; auto test = [&] (Texture const &texture, Rect rect, Rotate rotate, Flip flip, Expected const &expected) { Recorded::recorded = { }; _b2f(surface, texture, rect, rotate, flip); log("b2f: ", rect, " ", name(rotate), flip.enabled ? " flip" : "", " -> ", Recorded::recorded); if (Recorded::recorded != expected) { error("test_b2f_dispatch failed, expected: ", expected); throw 1; } }; log("offset calculation of destination window"); { unsigned const x = 32, y = 16, w = 64, h = 48; addr_t const src_landscape_ptr = y*640 + x, src_portrait_ptr = y*480 + x; Rect const rect { { x, y }, { w, h } }; test(texture_landscape, rect, Rotate::R0, Flip { }, expected(y*640 + x, 80, src_landscape_ptr, 80, 8, 6)); test(texture_landscape, rect, Rotate::R0, Flip { true }, expected(y*640 + 640 - w - x, 80, src_landscape_ptr, 80, 8, 6)); test(texture_portrait, rect, Rotate::R90, Flip { }, expected(x*640 + 640 - h - y, 80, src_portrait_ptr, 60, 8, 6)); test(texture_portrait, rect, Rotate::R90, Flip { true }, expected(x*640 + y, 80, src_portrait_ptr, 60, 8, 6)); test(texture_landscape, rect, Rotate::R180, Flip { }, expected((480 - y - h)*640 + 640 - x - w, 80, src_landscape_ptr, 80, 8, 6)); test(texture_landscape, rect, Rotate::R180, Flip { true }, expected((480 - y - h)*640 + x, 80, src_landscape_ptr, 80, 8, 6)); test(texture_portrait, rect, Rotate::R270, Flip { }, expected((480 - x - w)*640 + y, 80, src_portrait_ptr, 60, 8, 6)); test(texture_portrait, rect, Rotate::R270, Flip { true }, expected((480 - x - w)*640 + 640 - y - h, 80, src_portrait_ptr, 60, 8, 6)); } log("check for compatibility of surface and texture"); test(texture_portrait, { { }, { 16, 16 } }, Rotate::R0, Flip { }, expected(0, 0, 0, 0, 0, 0)); log("clamp rect to texture size"); test(texture_landscape, { { -99, -99 }, { 999, 999 } }, Rotate::R0, Flip { }, expected(0, 80, 0, 80, 80, 60)); log("ignore out-of-bounds rect"); test(texture_landscape, { { 1000, 0 }, { 16, 16 } }, Rotate::R0, Flip { }, expected(0, 0, 0, 0, 0, 0)); /* snap to grid */ log("snap rect argument to 8x8 grid"); test(texture_landscape, { { 31, 63 }, { 2, 2 } }, Rotate::R0, Flip { }, expected(56*640 + 24, 80, 56*640 + 24, 80, 2, 2)); } template static inline void test_simd_blend_mix() { struct Rgb : Genode::Hex { explicit Rgb(uint32_t v) : Hex(v, OMIT_PREFIX, PAD) { } }; struct Mix_test { uint32_t bg, fg; uint8_t a; uint32_t expected; void print(Output &out) const { Genode::print(out, "bg=", Rgb(bg), " fg=", Rgb(fg), " a=", a); } }; Mix_test mix_test[] { { .bg = 0x000000, .fg = 0x000000, .a = 0, .expected = 0x000000 }, { .bg = 0x000000, .fg = 0xffffff, .a = 0, .expected = 0x000000 }, { .bg = 0xffffff, .fg = 0x000000, .a = 0, .expected = 0xffffff }, { .bg = 0xffffff, .fg = 0xffffff, .a = 0, .expected = 0xffffff }, { .bg = 0x000000, .fg = 0x000000, .a = 255, .expected = 0x000000 }, { .bg = 0x000000, .fg = 0xffffff, .a = 255, .expected = 0xffffff }, { .bg = 0xffffff, .fg = 0x000000, .a = 255, .expected = 0x000000 }, { .bg = 0xffffff, .fg = 0xffffff, .a = 255, .expected = 0xffffff }, }; for (Mix_test const &test : mix_test) { uint32_t slow = Slow::Blend::_mix(test.bg, test.fg, test.a); uint32_t simd = SIMD::Blend::_mix(test.bg, test.fg, test.a); if (slow == test.expected && slow == simd) { log("mix ", test, " -> slow=", Rgb(slow), " simd=", Rgb(simd)); } else { error("mix ", test, " -> slow=", Rgb(slow), " simd=", Rgb(simd), " expected=", Rgb(test.expected)); throw 1; } } struct Xrgb_8x { uint32_t values[8]; void print(Output &out) const { for (unsigned i = 0; i < 8; i++) Genode::print(out, (i == 0) ? "" : ".", Rgb(values[i])); } bool operator != (Xrgb_8x const &other) const { for (unsigned i = 0; i < 8; i++) if (values[i] != other.values[i]) return true; return false; } }; uint32_t const ca = 0xaaaaaa, cb = 0xbbbbbb, cc = 0xcccccc, cd = 0xdddddd, white = 0xffffff; Xrgb_8x black_bg { }; Xrgb_8x white_bg { { white, white, white, white, white, white, white, white } }; Xrgb_8x fg { { 0x001020, 0x405060, 0x8090a0, 0xc0d0e0, ca, cb, cc, cd } }; uint8_t alpha[8] { 63, 127, 191, 255 , 64, 64, 64, 64 }; auto test_mix_8 = [&] (auto msg, Xrgb_8x &bg, Xrgb_8x const &fg, uint8_t const *alpha, Xrgb_8x const &expected) { log("fg : ", fg); log("bg : ", bg); SIMD::Blend::xrgb_a(bg.values, 8, fg.values, alpha); log(msg, " : ", bg); if (expected != bg) { error("expected ", expected); throw 1; } }; test_mix_8("blackened", black_bg, fg, alpha, { { 0x00000408, 0x00202830, 0x00606c78, 0x00c0d0e0, 0x002b2b2b, 0x002f2f2f, 0x00333333, 0x00383838 } }); test_mix_8("whitened ", white_bg, fg, alpha, { { 0x00c0c4c8, 0x00a0a8b0, 0x00a0acb8, 0x00c0d0e0, 0x00eaeaea, 0x00eeeeee, 0x00f3f3f3, 0x00f7f7f7 } }); } void Component::construct(Genode::Env &) { #ifdef _INCLUDE__BLIT__INTERNAL__NEON_H_ log("-- ARM Neon --"); test_simd_b2f(); test_simd_blend_mix(); #endif #ifdef _INCLUDE__BLIT__INTERNAL__SSE4_H_ log("-- SSE4 --"); test_simd_b2f(); test_simd_blend_mix(); #endif test_b2f_dispatch(); log("--- blit test finished ---"); }