From cd519c0db33f1bb2ceba0950479c4309c31b9ce8 Mon Sep 17 00:00:00 2001 From: Albert Slepak Date: Sat, 7 Nov 2026 00:32:20 -0800 Subject: [PATCH] feat(stlxgfx): added a straight alpha surface flag Every blend makes its pixel opaque, so a picture drawn onto a transparent surface loses the coverage it needs to be blended again. A flagged surface keeps each pixel's coverage in its alpha, as a host test in CI checks. --- .github/workflows/host-tests.yml | 38 ++ Makefile | 6 +- .../lib/libstlxgfx/include/stlxgfx/blend.h | 29 ++ .../lib/libstlxgfx/include/stlxgfx/surface.h | 4 + userland/lib/libstlxgfx/src/surface.c | 42 +- userland/lib/libstlxgfx/src/text.c | 6 + userland/lib/libstlxgfx/tests/Makefile | 28 ++ .../libstlxgfx/tests/straight_alpha.test.c | 398 ++++++++++++++++++ 8 files changed, 546 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/host-tests.yml create mode 100644 userland/lib/libstlxgfx/tests/Makefile create mode 100644 userland/lib/libstlxgfx/tests/straight_alpha.test.c diff --git a/.github/workflows/host-tests.yml b/.github/workflows/host-tests.yml new file mode 100644 index 000000000..ab5cbae13 --- /dev/null +++ b/.github/workflows/host-tests.yml @@ -0,0 +1,38 @@ +# +# Host Tests +# +# Builds the userland libraries' host tests with the runner's own compiler and +# runs them, for library code that no unit test inside Stellux reaches. +# + +name: Host tests + +# Pull requests run every branch under review, so pushes run only for master +on: + push: + branches: + - master + pull_request: + +permissions: + contents: read + +# A newer commit on a pull request cancels its older run, and each push keeps its own +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +jobs: + host-tests: + name: host tests + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Run host tests + run: make host-test diff --git a/Makefile b/Makefile index d519cea19..06ceb8d7b 100644 --- a/Makefile +++ b/Makefile @@ -142,7 +142,7 @@ endif # Primary Targets # ============================================================================ -.PHONY: all kernel userland image run run-headless clean test test-image \ +.PHONY: all kernel userland image run run-headless clean test test-image host-test \ image-x86_64 image-aarch64 \ run-qemu-x86_64 run-qemu-aarch64 \ run-qemu-x86_64-headless run-qemu-aarch64-headless \ @@ -186,6 +186,10 @@ test: test-image @echo "" $(Q)QEMU_X2APIC=$(QEMU_X2APIC) ./scripts/run_tests.sh $(ARCH) +# Builds and runs the userland libraries' tests on the build machine, needing no image +host-test: + $(Q)$(MAKE) --no-print-directory -C userland/lib/libstlxgfx/tests + # ============================================================================ # Disk Image Creation # ============================================================================ diff --git a/userland/lib/libstlxgfx/include/stlxgfx/blend.h b/userland/lib/libstlxgfx/include/stlxgfx/blend.h index 3c92c747b..325b9b77c 100644 --- a/userland/lib/libstlxgfx/include/stlxgfx/blend.h +++ b/userland/lib/libstlxgfx/include/stlxgfx/blend.h @@ -16,4 +16,33 @@ static inline uint8_t stlxgfx_blend_channel(uint8_t dst, uint32_t src_premul, return (uint8_t)stlxgfx_div255(src_premul + dst * inv_alpha); } +/* Source-over of `src` onto `dst` in straight alpha, where color is + * not multiplied by alpha. Both are ARGB, and `dst` alpha must be + * below 255, since opaque pixels blend by `stlxgfx_blend_channel`. + * Onto alpha 0 the result is `src`. Otherwise the alpha is the + * coverage of both pixels together, rounded up, and each channel the + * mean of both colors weighted by the coverage each shows, rounded, + * so it lies between them. */ +static inline uint32_t stlxgfx_blend_straight(uint32_t dst, uint32_t src) { + uint32_t src_alpha = src >> 24; + uint32_t dst_alpha = dst >> 24; + if (dst_alpha == 0) { + return src; + } + + uint32_t src_weight = src_alpha * 255; + uint32_t dst_weight = dst_alpha * (255 - src_alpha); + uint32_t weight = src_weight + dst_weight; + uint32_t blended = (src_alpha + (dst_weight + 254) / 255) << 24; + + for (uint32_t shift = 0; shift < 24; shift += 8) { + uint32_t src_channel = (src >> shift) & 0xFF; + uint32_t dst_channel = (dst >> shift) & 0xFF; + uint32_t sum = src_channel * src_weight + dst_channel * dst_weight; + blended |= ((sum + weight / 2) / weight) << shift; + } + + return blended; +} + #endif /* STLXGFX_BLEND_H */ diff --git a/userland/lib/libstlxgfx/include/stlxgfx/surface.h b/userland/lib/libstlxgfx/include/stlxgfx/surface.h index 8ee37e5d8..8a3dd6b6a 100644 --- a/userland/lib/libstlxgfx/include/stlxgfx/surface.h +++ b/userland/lib/libstlxgfx/include/stlxgfx/surface.h @@ -8,6 +8,9 @@ extern "C" { #endif +/* `flags` bit: blends onto 32 bpp pixels below alpha 255 use `stlxgfx_blend_straight`. */ +#define STLXGFX_SURFACE_STRAIGHT_ALPHA (1u << 0) + typedef struct { uint32_t width; uint32_t height; @@ -18,6 +21,7 @@ typedef struct { uint8_t blue_shift; uint8_t* pixels; int owned; + uint32_t flags; } stlxgfx_surface_t; static inline uint8_t stlxgfx_alpha_byte_index(const stlxgfx_surface_t* s) { diff --git a/userland/lib/libstlxgfx/src/surface.c b/userland/lib/libstlxgfx/src/surface.c index 032b4d963..3d7dc2ce7 100644 --- a/userland/lib/libstlxgfx/src/surface.c +++ b/userland/lib/libstlxgfx/src/surface.c @@ -47,6 +47,7 @@ stlxgfx_surface_t* stlxgfx_create_surface(uint32_t width, uint32_t height, s->green_shift = green_shift; s->blue_shift = blue_shift; s->owned = 1; + s->flags = 0; s->pixels = malloc((size_t)s->pitch * height); if (!s->pixels) { @@ -94,6 +95,7 @@ stlxgfx_surface_t* stlxgfx_surface_from_buffer(uint8_t* buffer, s->blue_shift = blue_shift; s->pixels = buffer; s->owned = 0; + s->flags = 0; return s; } @@ -341,6 +343,14 @@ static inline void blend_pixel(uint8_t* dst_px, const stlxgfx_surface_t* dst, return; } + if (dst->flags & STLXGFX_SURFACE_STRAIGHT_ALPHA) { + uint32_t below = read_pixel(dst_px, dst); + if ((below >> 24) != 0xFF) { + write_pixel(dst_px, dst, stlxgfx_blend_straight(below, src_color)); + return; + } + } + uint32_t sr = (src_color >> 16) & 0xFF; uint32_t sg = (src_color >> 8) & 0xFF; uint32_t sb = src_color & 0xFF; @@ -387,13 +397,25 @@ int stlxgfx_fill_rect_blend(stlxgfx_surface_t* s, int32_t x, int32_t y, return 0; } + uint32_t bytes_pp = s->bpp / 8; + if (s->flags & STLXGFX_SURFACE_STRAIGHT_ALPHA) { + for (int32_t row = y0; row < y1; row++) { + uint8_t* px = s->pixels + (uint32_t)row * s->pitch + + (uint32_t)x0 * bytes_pp; + for (int32_t col = x0; col < x1; col++, px += bytes_pp) { + blend_pixel(px, s, color); + } + } + + return 0; + } + /* Full-screen dim hot path. Packed whole-word blending is what * stays fast under emulation, measured by gfxbench. */ uint32_t inv = 255 - (uint32_t)sa; uint32_t spr = ((color >> 16) & 0xFF) * (uint32_t)sa; uint32_t spg = ((color >> 8) & 0xFF) * (uint32_t)sa; uint32_t spb = ( color & 0xFF) * (uint32_t)sa; - uint32_t bytes_pp = s->bpp / 8; if (bytes_pp == 4) { uint32_t rs = s->red_shift; @@ -486,10 +508,22 @@ int stlxgfx_blit_alpha(stlxgfx_surface_t* dst, int32_t dx, int32_t dy, return 0; } - /* Per-pixel source alpha: hoist both surfaces' channel offsets and - * read source channels directly, skipping the packed round trip. */ uint32_t dst_bpp = dst->bpp / 8; uint32_t src_bpp = src->bpp / 8; + if (dst->flags & STLXGFX_SURFACE_STRAIGHT_ALPHA) { + for (int32_t row = 0; row < sh; row++) { + const uint8_t* sp = src->pixels + ((uint32_t)sy + (uint32_t)row) * src->pitch + (uint32_t)sx * src_bpp; + uint8_t* dp = dst->pixels + ((uint32_t)dy + (uint32_t)row) * dst->pitch + (uint32_t)dx * dst_bpp; + for (int32_t col = 0; col < sw; col++, sp += src_bpp, dp += dst_bpp) { + blend_pixel(dp, dst, read_pixel(sp, src)); + } + } + + return 0; + } + + /* Per-pixel source alpha: hoist both surfaces' channel offsets and + * read source channels directly, skipping the packed round trip. */ uint8_t s_rb = src->red_shift / 8; uint8_t s_gb = src->green_shift / 8; uint8_t s_bb = src->blue_shift / 8; @@ -763,7 +797,7 @@ void stlxgfx_blit_arc_corner(stlxgfx_surface_t* dst, int32_t x, int32_t y, + (uint32_t)px * src_bpp, src); blend_pixel(dst->pixels + (uint32_t)py * dst->pitch + (uint32_t)px * dst_bpp, - dst, (cov << 24) | (sample & 0x00FFFFFF)); + dst, ((uint32_t)cov << 24) | (sample & 0x00FFFFFF)); } } } diff --git a/userland/lib/libstlxgfx/src/text.c b/userland/lib/libstlxgfx/src/text.c index 98869b1b1..a583de934 100644 --- a/userland/lib/libstlxgfx/src/text.c +++ b/userland/lib/libstlxgfx/src/text.c @@ -527,6 +527,7 @@ void stlxgfx_draw_text(stlxgfx_surface_t* s, const stlxgfx_font* font, uint8_t bb = s->blue_shift / 8; uint8_t ab = stlxgfx_alpha_byte_index(s); int has_alpha = bytes_pp == 4; + int keeps_straight_alpha = (s->flags & STLXGFX_SURFACE_STRAIGHT_ALPHA) != 0; uint32_t src_r = (color >> 16) & 0xFF; uint32_t src_g = (color >> 8) & 0xFF; uint32_t src_b = color & 0xFF; @@ -584,6 +585,11 @@ void stlxgfx_draw_text(stlxgfx_surface_t* s, const stlxgfx_font* font, continue; } + if (keeps_straight_alpha) { + stlxgfx_blend_coverage(s, sx, sy, color, cov[c]); + continue; + } + uint32_t a = cov[c]; if (a == 0) { continue; diff --git a/userland/lib/libstlxgfx/tests/Makefile b/userland/lib/libstlxgfx/tests/Makefile new file mode 100644 index 000000000..9cf753bdd --- /dev/null +++ b/userland/lib/libstlxgfx/tests/Makefile @@ -0,0 +1,28 @@ +# +# Stellux Userland libstlxgfx Host Tests +# +# Builds each *.test.c with the sources in `SOURCES` by the build machine's own +# compiler and runs it. The top-level `make host-test` runs this. +# + +HOST_CC ?= clang + +REPO_ROOT := $(abspath ../../../..) +SRC_DIR := ../src +BUILD_DIR := ../build/host +SOURCES := $(SRC_DIR)/surface.c $(SRC_DIR)/text.c +HEADERS := $(wildcard ../include/stlxgfx/*.h ../include/stlxgfx/internal/*.h) +TESTS := $(patsubst %.test.c,$(BUILD_DIR)/%,$(wildcard *.test.c)) + +CFLAGS := -std=c11 -O2 -g -Wall -Wextra -Werror -I../include \ + -DINITRD_DIR='"$(REPO_ROOT)/initrd"' + +run: $(TESTS) + @for test in $(TESTS); do echo "[RUN] $$test"; $$test || exit 1; done + +$(BUILD_DIR)/%: %.test.c $(SOURCES) $(HEADERS) + @mkdir -p $(BUILD_DIR) + @echo "[CC] $< (host)" + @$(HOST_CC) $(CFLAGS) $< $(SOURCES) -o $@ -lm + +.PHONY: run diff --git a/userland/lib/libstlxgfx/tests/straight_alpha.test.c b/userland/lib/libstlxgfx/tests/straight_alpha.test.c new file mode 100644 index 000000000..48aed8684 --- /dev/null +++ b/userland/lib/libstlxgfx/tests/straight_alpha.test.c @@ -0,0 +1,398 @@ +/* Each blending call onto straight alpha surfaces, run on the build machine over transparent, + * partly covered and opaque pixels and checked against the rule restated here */ + +#include +#include + +#include +#include +#include + +#define SWEEP_WIDTH 256 +#define SWEEP_HEIGHT 3 +#define SHAPE_SIZE 64 +#define BAND_HEIGHT 16 +#define TEXT_WIDTH 160 +#define TEXT_HEIGHT 32 +#define FONT_PX 20 +#define MAX_REPORTS 8 +#define OPAQUE 0xFF000000u +#define TRANSLUCENT 0x80000000u + +/* One blending call, drawing color `rgb` at the alpha each pixel gets from the call's geometry or + * source, each pixel at most once, since two draws stacked and blended once round differently */ +typedef struct { + const char* name; + uint32_t width; + uint32_t height; + void (*draw)(stlxgfx_surface_t* dst, uint32_t rgb); + int alpha_is_column; +} draw_call; + +typedef struct { + uint32_t drawn; + uint32_t below; +} color_pair; + +static stlxgfx_font* g_font; +static uint64_t g_checks; +static uint64_t g_failures; + +static stlxgfx_surface_t* new_surface( + uint32_t width, + uint32_t height, + uint32_t flags, + uint32_t argb +) { + stlxgfx_surface_t* s = stlxgfx_create_surface(width, height, 32, 16, 8, 0); + if (!s) { + fprintf(stderr, "straight alpha: out of memory\n"); + exit(1); + } + + s->flags = flags; + stlxgfx_clear(s, argb); + return s; +} + +/* A source whose alpha varies along both rows and columns */ +static stlxgfx_surface_t* new_gradient(uint32_t width, uint32_t height, uint32_t rgb) { + stlxgfx_surface_t* src = new_surface(width, height, 0, 0); + for (uint32_t y = 0; y < height; y++) { + for (uint32_t x = 0; x < width; x++) { + uint32_t alpha = (x * 4 + y * 3) & 0xFF; + stlxgfx_fill_rect(src, (int32_t)x, (int32_t)y, 1, 1, (alpha << 24) | rgb); + } + } + + return src; +} + +static uint32_t pixel_at(const stlxgfx_surface_t* s, uint32_t x, uint32_t y) { + const uint8_t* p = s->pixels + y * s->pitch + x * 4; + return ((uint32_t)p[3] << 24) | ((uint32_t)p[2] << 16) | ((uint32_t)p[1] << 8) | p[0]; +} + +static void sweep_blend_coverage(stlxgfx_surface_t* dst, uint32_t argb) { + for (uint32_t y = 0; y < SWEEP_HEIGHT; y++) { + for (uint32_t x = 0; x < SWEEP_WIDTH; x++) { + stlxgfx_blend_coverage(dst, (int32_t)x, (int32_t)y, argb, (uint8_t)x); + } + } +} + +static void draw_blend_coverage(stlxgfx_surface_t* dst, uint32_t rgb) { + sweep_blend_coverage(dst, OPAQUE | rgb); +} + +static void draw_translucent_coverage(stlxgfx_surface_t* dst, uint32_t rgb) { + sweep_blend_coverage(dst, TRANSLUCENT | rgb); +} + +static void draw_fill_rect_blend(stlxgfx_surface_t* dst, uint32_t rgb) { + for (uint32_t x = 0; x < SWEEP_WIDTH; x++) { + stlxgfx_fill_rect_blend(dst, (int32_t)x, 0, 1, SWEEP_HEIGHT, (x << 24) | rgb); + } +} + +/* Bands past the surface's sides, the first also above it, so each fill blends whole rows from + * clipped bounds */ +static void draw_fill_rect_blend_bands(stlxgfx_surface_t* dst, uint32_t rgb) { + static const uint32_t alphas[] = { 1, 128, 254, 255 }; + + for (uint32_t i = 0; i < sizeof(alphas) / sizeof(alphas[0]); i++) { + int32_t top = i == 0 ? -4 : (int32_t)(i * BAND_HEIGHT); + int32_t bottom = (int32_t)((i + 1) * BAND_HEIGHT); + uint32_t argb = (alphas[i] << 24) | rgb; + stlxgfx_fill_rect_blend(dst, -3, top, SHAPE_SIZE + 6, (uint32_t)(bottom - top), argb); + } +} + +static void draw_blit_alpha(stlxgfx_surface_t* dst, uint32_t rgb) { + stlxgfx_surface_t* src = new_surface(SWEEP_WIDTH, SWEEP_HEIGHT, 0, 0); + for (uint32_t x = 0; x < SWEEP_WIDTH; x++) { + stlxgfx_fill_rect(src, (int32_t)x, 0, 1, SWEEP_HEIGHT, (x << 24) | rgb); + } + + stlxgfx_blit_alpha(dst, 0, 0, src, 0, 0, SWEEP_WIDTH, SWEEP_HEIGHT); + stlxgfx_destroy_surface(src); +} + +/* Part of a wider source, from an offset onto an origin the surface clips */ +static void draw_blit_alpha_offset(stlxgfx_surface_t* dst, uint32_t rgb) { + stlxgfx_surface_t* src = new_gradient(SHAPE_SIZE + 16, SHAPE_SIZE, rgb); + stlxgfx_blit_alpha(dst, -2, 4, src, 5, 3, SHAPE_SIZE, SHAPE_SIZE - 8); + stlxgfx_destroy_surface(src); +} + +static void draw_blit_rounded_alpha(stlxgfx_surface_t* dst, uint32_t rgb) { + stlxgfx_surface_t* src = new_gradient(SHAPE_SIZE, SHAPE_SIZE, rgb); + stlxgfx_blit_rounded_alpha(dst, 0, 0, src, 0, 0, SHAPE_SIZE, SHAPE_SIZE, 20); + stlxgfx_destroy_surface(src); +} + +static void draw_blit_arc_corner(stlxgfx_surface_t* dst, uint32_t rgb) { + stlxgfx_surface_t* src = new_surface(SHAPE_SIZE, SHAPE_SIZE, 0, OPAQUE | rgb); + stlxgfx_blit_arc_corner(dst, 0, 0, 48, 20, 1, 1, 0, src); + stlxgfx_destroy_surface(src); +} + +static void write_text(stlxgfx_surface_t* dst, uint32_t argb) { + static const char text[] = "Stellux 0.1"; + stlxgfx_draw_text(dst, g_font, 2, 24, text, strlen(text), argb); +} + +static void draw_text(stlxgfx_surface_t* dst, uint32_t rgb) { + write_text(dst, OPAQUE | rgb); +} + +static void draw_translucent_text(stlxgfx_surface_t* dst, uint32_t rgb) { + write_text(dst, TRANSLUCENT | rgb); +} + +static void expect( + int holds, + const char* call, + const char* what, + uint32_t x, + uint32_t y, + uint32_t got, + uint32_t want +) { + g_checks++; + if (holds) { + return; + } + + g_failures++; + if (g_failures <= MAX_REPORTS) { + printf(" FAIL: %s, %s at (%u, %u): 0x%08X, expected 0x%08X\n", + call, what, x, y, got, want); + } +} + +/* `c` drawn at alpha `s` onto alpha `d` below 255 stores the coverage of both together, rounded up, + * and each channel the mean of both colors weighted by the coverage each shows, rounded */ +static uint32_t straight_over(uint32_t below, uint32_t c, uint32_t s) { + uint32_t d = below >> 24; + uint32_t w = s * 255 + d * (255 - s); + uint32_t a = s + (d * (255 - s) + 254) / 255; + + uint32_t over = a << 24; + for (uint32_t shift = 0; shift < 24; shift += 8) { + uint32_t drawn = (c >> shift) & 0xFF; + uint32_t kept = (below >> shift) & 0xFF; + over |= ((drawn * s * 255 + kept * d * (255 - s) + w / 2) / w) << shift; + } + + return over; +} + +static int channels_between(uint32_t got, uint32_t c, uint32_t e) { + for (uint32_t shift = 0; shift < 24; shift += 8) { + uint32_t channel = (got >> shift) & 0xFF; + uint32_t drawn = (c >> shift) & 0xFF; + uint32_t kept = (e >> shift) & 0xFF; + uint32_t low = drawn < kept ? drawn : kept; + uint32_t high = drawn < kept ? kept : drawn; + if (channel < low || channel > high) { + return 0; + } + } + + return 1; +} + +/* Onto transparent pixels each drawn pixel stores the call's color, at its column's alpha when + * `alpha_is_column`, and the rest stay as they were */ +static void check_single_draws( + const draw_call* call, + const stlxgfx_surface_t* picture, + color_pair colors +) { + uint32_t partial = 0; + + for (uint32_t y = 0; y < call->height; y++) { + for (uint32_t x = 0; x < call->width; x++) { + uint32_t got = pixel_at(picture, x, y); + uint32_t s = got >> 24; + uint32_t want = s == 0 ? colors.below : (s << 24) | colors.drawn; + expect(got == want, call->name, "single draw", x, y, got, want); + + if (call->alpha_is_column) { + expect(s == x, call->name, "stored alpha", x, y, got, (x << 24) | colors.drawn); + } + + partial += s != 0 && s != 255; + } + } + + g_checks++; + if (partial == 0) { + g_failures++; + printf(" FAIL: %s, no partly covered pixel drawn\n", call->name); + } +} + +/* Blending the picture once onto an opaque surface gives what the same + * call draws directly onto that surface */ +static void check_blended_once( + const draw_call* call, + const stlxgfx_surface_t* picture, + color_pair colors +) { + uint32_t backgrounds[] = { OPAQUE, 0xFFFFFFFFu, OPAQUE | colors.below }; + + for (size_t i = 0; i < sizeof(backgrounds) / sizeof(backgrounds[0]); i++) { + stlxgfx_surface_t* drawn = new_surface(call->width, call->height, 0, backgrounds[i]); + stlxgfx_surface_t* blended = new_surface(call->width, call->height, 0, backgrounds[i]); + + call->draw(drawn, colors.drawn); + stlxgfx_blit_alpha(blended, 0, 0, picture, 0, 0, call->width, call->height); + + for (uint32_t y = 0; y < call->height; y++) { + for (uint32_t x = 0; x < call->width; x++) { + uint32_t got = pixel_at(blended, x, y); + uint32_t want = pixel_at(drawn, x, y); + expect(got == want, call->name, "picture blended once", x, y, got, want); + } + } + + stlxgfx_destroy_surface(drawn); + stlxgfx_destroy_surface(blended); + } +} + +/* Over alpha 1 to 254 a draw follows the rule, stores no less alpha than both draws cover and keeps + * each channel between both colors, so a color drawn over itself stays */ +static void check_stacked_draws( + const draw_call* call, + const stlxgfx_surface_t* picture, + uint32_t c, + uint32_t below +) { + stlxgfx_surface_t* stacked = new_surface( + call->width, + call->height, + STLXGFX_SURFACE_STRAIGHT_ALPHA, + below + ); + uint32_t d = below >> 24; + + call->draw(stacked, c); + + for (uint32_t y = 0; y < call->height; y++) { + for (uint32_t x = 0; x < call->width; x++) { + uint32_t got = pixel_at(stacked, x, y); + uint32_t s = pixel_at(picture, x, y) >> 24; + if (s == 0) { + expect(got == below, call->name, "pixel left as it was", x, y, got, below); + continue; + } + + uint32_t want = straight_over(below, c, s); + uint32_t a = got >> 24; + int is_between = channels_between(got, c, below); + int covers_both = a * 255 >= s * 255 + d * (255 - s); + + expect(got == want, call->name, "stacked draw", x, y, got, want); + expect(is_between, call->name, "channels between both colors", x, y, got, want); + expect(covers_both, call->name, "alpha covering both draws", x, y, got, want); + } + } + + stlxgfx_destroy_surface(stacked); +} + +/* Onto opaque pixels the flag changes nothing */ +static void check_opaque_draws(const draw_call* call, color_pair colors) { + uint32_t opaque = OPAQUE | colors.below; + stlxgfx_surface_t* flagged = new_surface( + call->width, + call->height, + STLXGFX_SURFACE_STRAIGHT_ALPHA, + opaque + ); + stlxgfx_surface_t* plain = new_surface(call->width, call->height, 0, opaque); + + call->draw(flagged, colors.drawn); + call->draw(plain, colors.drawn); + + for (uint32_t y = 0; y < call->height; y++) { + for (uint32_t x = 0; x < call->width; x++) { + uint32_t got = pixel_at(flagged, x, y); + uint32_t want = pixel_at(plain, x, y); + expect(got == want, call->name, "opaque pixel as without the flag", x, y, got, want); + } + } + + stlxgfx_destroy_surface(flagged); + stlxgfx_destroy_surface(plain); +} + +static void check_call(const draw_call* call, color_pair colors) { + stlxgfx_surface_t* picture = new_surface( + call->width, + call->height, + STLXGFX_SURFACE_STRAIGHT_ALPHA, + colors.below + ); + + call->draw(picture, colors.drawn); + check_single_draws(call, picture, colors); + check_blended_once(call, picture, colors); + + for (uint32_t d = 1; d < 255; d++) { + check_stacked_draws(call, picture, colors.drawn, (d << 24) | colors.below); + } + + check_opaque_draws(call, colors); + stlxgfx_destroy_surface(picture); +} + +int main(void) { + static const char font_path[] = INITRD_DIR STLXGFX_FONT_PATH; + static const draw_call calls[] = { + { "blend_coverage", SWEEP_WIDTH, SWEEP_HEIGHT, draw_blend_coverage, 1 }, + { "blend_coverage translucent", SWEEP_WIDTH, SWEEP_HEIGHT, draw_translucent_coverage, 0 }, + { "fill_rect_blend", SWEEP_WIDTH, SWEEP_HEIGHT, draw_fill_rect_blend, 1 }, + { "fill_rect_blend bands", SHAPE_SIZE, SHAPE_SIZE, draw_fill_rect_blend_bands, 0 }, + { "blit_alpha", SWEEP_WIDTH, SWEEP_HEIGHT, draw_blit_alpha, 1 }, + { "blit_alpha offset", SHAPE_SIZE, SHAPE_SIZE, draw_blit_alpha_offset, 0 }, + { "blit_rounded_alpha", SHAPE_SIZE, SHAPE_SIZE, draw_blit_rounded_alpha, 0 }, + { "blit_arc_corner", SHAPE_SIZE, SHAPE_SIZE, draw_blit_arc_corner, 0 }, + { "draw_text", TEXT_WIDTH, TEXT_HEIGHT, draw_text, 0 }, + { "draw_text translucent", TEXT_WIDTH, TEXT_HEIGHT, draw_translucent_text, 0 }, + }; + + /* The first pair's red is drawn over less red, its green over the same and its blue over more. + * The second draws a color over itself, and the third black over white */ + static const color_pair pairs[] = { + { 0xFF8020u, 0x2080FFu }, + { 0x2080FFu, 0x2080FFu }, + { 0x000000u, 0xFFFFFFu }, + }; + + g_font = stlxgfx_font_open(font_path, FONT_PX); + if (!g_font) { + fprintf(stderr, "straight alpha: cannot open %s\n", font_path); + return 1; + } + + for (size_t i = 0; i < sizeof(calls) / sizeof(calls[0]); i++) { + for (size_t j = 0; j < sizeof(pairs) / sizeof(pairs[0]); j++) { + check_call(&calls[i], pairs[j]); + } + } + + stlxgfx_font_close(g_font); + + if (g_failures != 0) { + printf("straight alpha: %llu of %llu checks failed\n", + (unsigned long long)g_failures, (unsigned long long)g_checks); + return 1; + } + + printf("straight alpha: %llu checks passed\n", (unsigned long long)g_checks); + return 0; +}