diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:13:27 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:13:27 +0000 |
commit | 40a355a42d4a9444dc753c04c6608dade2f06a23 (patch) | |
tree | 871fc667d2de662f171103ce5ec067014ef85e61 /dom/canvas/test/reftest/colors/generate_color_canvas_reftests.py | |
parent | Adding upstream version 124.0.1. (diff) | |
download | firefox-adbda400be353e676059e335c3c0aaf99e719475.tar.xz firefox-adbda400be353e676059e335c3c0aaf99e719475.zip |
Adding upstream version 125.0.1.upstream/125.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/canvas/test/reftest/colors/generate_color_canvas_reftests.py')
-rw-r--r-- | dom/canvas/test/reftest/colors/generate_color_canvas_reftests.py | 427 |
1 files changed, 427 insertions, 0 deletions
diff --git a/dom/canvas/test/reftest/colors/generate_color_canvas_reftests.py b/dom/canvas/test/reftest/colors/generate_color_canvas_reftests.py new file mode 100644 index 0000000000..8c1e5f3788 --- /dev/null +++ b/dom/canvas/test/reftest/colors/generate_color_canvas_reftests.py @@ -0,0 +1,427 @@ +#! python3 + +# Typecheck: +# `pip -m mypy generate_color_canvas_reftests.py` +# +# Run: +# `./generate_color_canvas_reftests.py [--write]` + +import functools +import json +import math +import pathlib +import re +import sys +from typing import Iterable, NamedTuple, TypeVar + +ARGS = sys.argv[1:] +DEST = pathlib.Path(__file__).parent / "_generated_reftest.list" + +COL_DELIM = " " +COL_ALIGNMENT = 4 + +# - + +T = TypeVar("T") +U = TypeVar("U") + +# - + + +# crossCombine([{a:false},{a:5}], [{},{b:5}]) +# [{a:false}, {a:true}, {a:false,b:5}, {a:true,b:5}] +def cross_combine(*args_tup: list[dict]) -> list[dict]: + args = list(args_tup) + for i, a in enumerate(args): + assert type(a) == list, f"Arg{i} is {type(a)}, expected {list}." + + def cross_combine2(listA, listB): + listC = [] + for a in listA: + for b in listB: + c = dict() + c.update(a) + c.update(b) + listC.append(c) + return listC + + res: list[dict] = [dict()] + while True: + try: + next = args.pop(0) + except IndexError: + break + res = cross_combine2(res, next) + return res + + +# keyed_alternatives('count', [1,2,3]) -> [{count: 1}, {count: 2}, {count: 3}] +def keyed_alternatives(key: T, vals: Iterable[U]) -> list[dict[T, U]]: + """ + res = [] + for v in vals: + d = dict() + d[key] = v + res.append(d) + return res + """ + # return [dict([[key,v]]) for v in vals] + return [{key: v} for v in vals] + + +# - + + +def eprint(*args, **kwargs): + sys.stdout.flush() + print(*args, file=sys.stderr, **kwargs) + sys.stderr.flush() + + +# - + +# color_canvas.html?e_width=100&e_height=100&e_context=css&e_options={}&e_cspace=&e_webgl_format=&e_color=rgb(127,0,0) + +CSPACE_LIST = ["srgb", "display-p3"] +CANVAS_CSPACES = keyed_alternatives("e_cspace", CSPACE_LIST) + +RGB_LIST = [ + "0.000 0.000 0.000", + "0.200 0.200 0.200", # 0.2*255 = 51 + "0.200 0.000 0.000", + "0.000 0.200 0.000", + "0.000 0.000 0.200", + "0.502 0.502 0.502", # 0.502*255 = 128.01 + "0.502 0.000 0.000", + "0.000 0.502 0.000", + "0.000 0.000 0.502", + "1.000 1.000 1.000", + #'1.000 0.000 0.000', # These will hit gamut clipping on most displays. + #'0.000 1.000 0.000', + #'0.000 0.000 1.000', +] + +WEBGL_COLORS = keyed_alternatives("e_color", [f"color(srgb {rgb})" for rgb in RGB_LIST]) + +C2D_COLORS = [] +for cspace in CSPACE_LIST: + C2D_COLORS += keyed_alternatives( + "e_color", [f"color({cspace} {rgb})" for rgb in RGB_LIST] + ) + +# - + +WEBGL_FORMATS = keyed_alternatives( + "e_webgl_format", + [ + "RGBA8", + # Bug 1883748: (webgl.drawingbufferStorage) + #'SRGB8_ALPHA8', + #'RGBA16F', + ], +) +WEBGL = cross_combine( + [{"e_context": "webgl"}], WEBGL_FORMATS, CANVAS_CSPACES, WEBGL_COLORS +) + +# - + +C2D_OPTIONS_COMBOS = cross_combine( + keyed_alternatives("willReadFrequently", ["true", "false"]), # E.g. D2D vs Skia + # keyed_alternatives('alpha', ['true','false']) +) +C2D_OPTIONS = [ + json.dumps(config, separators=(",", ":")) for config in C2D_OPTIONS_COMBOS +] + +C2D = cross_combine( + [{"e_context": "2d"}], + keyed_alternatives("e_options", C2D_OPTIONS), + CANVAS_CSPACES, + C2D_COLORS, +) + +# - + +COMBOS: list[dict[str, str]] = cross_combine(WEBGL + C2D) + +eprint(f"{len(COMBOS)} combinations...") + +# - + +Config = dict[str, str] + + +class CssColor(NamedTuple): + cspace: str + rgb: str + + def rgb_vals(self) -> tuple[float, float, float]: + (r, g, b) = [float(z) for z in self.rgb.split(" ")] + return (r, g, b) + + def is_same_color(x, y) -> bool: + if x == y: + return True + (r, g, b) = x.rgb_vals() + if x.rgb == y.rgb and r == g and g == b: + return True + return False + + +class Reftest(NamedTuple): + notes: list[str] + op: str + test_config: Config + ref_config: Config + + +def make_ref_config(color: CssColor) -> Config: + return { + "e_context": "css", + "e_color": f"color({color.cspace} {color.rgb})", + } + + +class ColorReftest(NamedTuple): + notes: list[str] + test_config: Config + ref_color: CssColor + + def to_reftest(self): + ref_config = make_ref_config(self.ref_color) + return Reftest(self.notes.copy(), "==", self.test_config.copy(), ref_config) + + +class Expectation(NamedTuple): + notes: list[str] + test_config: Config + color: CssColor + + +def parse_css_color(s: str) -> CssColor: + m = re.match("color[(]([^)]+)[)]", s) + assert m, s + (cspace, rgb) = m.group(1).split(" ", 1) + return CssColor(cspace, rgb) + + +def correct_color_from_test_config(test_config: Config) -> CssColor: + canvas_cspace = test_config["e_cspace"] + if not canvas_cspace: + canvas_cspace = "srgb" + + correct_color = parse_css_color(test_config["e_color"]) + if test_config["e_context"] == "webgl": + # Webgl ignores the color's cspace, because webgl has no concept of + # source colorspace for clears/draws to the backbuffer. + # This (correct) behavior is as if the color's cspace were overwritten by the + # cspace of the canvas. (so expect that) + correct_color = CssColor(canvas_cspace, correct_color.rgb) + + return correct_color + + +# ------------------------------------- +# ------------------------------------- +# ------------------------------------- +# Choose (multiple?) reference configs given a test config. + + +def reftests_from_config(test_config: Config) -> Iterable[ColorReftest]: + correct_color = correct_color_from_test_config(test_config) + + if test_config["e_context"] == "2d": + # Canvas2d generally has the same behavior as css, so expect all passing. + yield ColorReftest([], test_config, correct_color) + return + + assert test_config["e_context"] == "webgl", test_config["e_context"] + + # - + + def reftests_from_expected_color( + notes: list[str], expected_color: CssColor + ) -> Iterable[ColorReftest]: + # If expecting failure, generate two tests, both expecting failure: + # 1. expected-fail test == correct_color + # 2. expected-pass test == (incorrect) expected_color + # If we fix an error, we'll see one unexpected-pass and one unexpected-fail. + # If we get a new wrong answer, we'll see one unexpected-fail. + + if not expected_color.is_same_color(correct_color): + yield ColorReftest(notes + ["fails"], test_config, correct_color) + yield ColorReftest(notes, test_config, expected_color) + else: + yield ColorReftest(notes, test_config, correct_color) + + # - + # On Mac, (with the pref) we do tag the IOSurface with the right cspace. + # On other platforms, Webgl always outputs in the display color profile + # right now. This is the same as "srgb". + + expected_color_srgb = CssColor("srgb", correct_color.rgb) + # Mac + yield from reftests_from_expected_color(["skip-if(!cocoaWidget)"], correct_color) + # Win, Lin, Android + yield from reftests_from_expected_color( + ["skip-if(cocoaWidget) "], expected_color_srgb + ) + + +# - + + +def amended_notes_from_reftest(reftest: ColorReftest) -> list[str]: + notes = reftest.notes[:] + + ref_rgb_vals = reftest.ref_color.rgb_vals() + is_green_only = ref_rgb_vals == (0, ref_rgb_vals[1], 0) + if ( + "fails" in reftest.notes + and reftest.test_config["e_context"] == "webgl" + and reftest.test_config["e_cspace"] == "display-p3" + and is_green_only + ): + # Android's display bitdepth rounds srgb green and p3 green to the same thing. + notes[notes.index("fails")] = "fails-if(!Android)" + + return notes + + +# ------------------------------------- +# ------------------------------------- +# ------------------------------------- +# Ok, back to implementation. + + +def encode_url_v(k, v): + if k == "e_color": + # reftest harness can't deal with spaces in urls, and 'color(srgb%201%200%200)' is hard to read. + v = v.replace(" ", ",") + + assert " " not in v, (k, v) + return v + + +# Cool: +assert encode_url_v("e_color", "color(srgb 0 0 0)") == "color(srgb,0,0,0)" +# Unfortunate, but tolerable: +assert encode_url_v("e_color", "color(srgb 0 0 0)") == "color(srgb,,,0,,,0,,,0)" + +# - + + +def url_from_config(kvs: Config) -> str: + parts = [f"{k}={encode_url_v(k,v)}" for k, v in kvs.items()] + url = "color_canvas.html?" + "&".join(parts) + return url + + +# - + +color_reftests: list[ColorReftest] = [] +for c in COMBOS: + color_reftests += reftests_from_config(c) +color_reftests = [ + ColorReftest(amended_notes_from_reftest(r), r.test_config, r.ref_color) + for r in color_reftests +] +reftests = [r.to_reftest() for r in color_reftests] + +# - + +HEADINGS = ["# annotations", "op", "test", "reference"] +table: list[list[str]] = [HEADINGS] +table = [ + [ + " ".join(r.notes), + r.op, + url_from_config(r.test_config), + url_from_config(r.ref_config), + ] + for r in reftests +] + +# - + + +def round_to(a, b: int) -> int: + return int(math.ceil(a / b) * b) + + +def aligned_lines_from_table( + rows: list[list[str]], col_delim=" ", col_alignment=4 +) -> Iterable[str]: + max_col_len = functools.reduce( + lambda accum, input: [max(r, len(c)) for r, c in zip(accum, input)], + rows, + [0 for _ in rows[0]], + ) + max_col_len = [round_to(x, col_alignment) for x in max_col_len] + + for i, row in enumerate(rows): + parts = [s + (" " * (col_len - len(s))) for s, col_len in zip(row, max_col_len)] + line = col_delim.join(parts) + yield line + + +# - + +GENERATED_FILE_LINE = "### Generated, do not edit. ###" + +lines = list(aligned_lines_from_table(table, COL_DELIM, COL_ALIGNMENT)) +WARN_EVERY_N_LINES = 5 +i = WARN_EVERY_N_LINES - 1 +while i < len(lines): + lines.insert(i, " " + GENERATED_FILE_LINE) + i += WARN_EVERY_N_LINES + +# - + +GENERATED_BY_ARGS = [f"./{pathlib.Path(__file__).name}"] + ARGS + +REFTEST_LIST_PREAMBLE = f"""\ +{GENERATED_FILE_LINE} + {GENERATED_FILE_LINE} + {GENERATED_FILE_LINE} + +# Generated by `{' '.join(GENERATED_BY_ARGS)}`. +# - + +defaults pref(webgl.colorspaces.prototype,true) + +{GENERATED_FILE_LINE} +# - + +# Ensure not white-screening: +!= {url_from_config({})+'='} about:blank +# Ensure differing results with different args: +!= {url_from_config({'e_color':'color(srgb 1 0 0)'})} {url_from_config({'e_color':'color(srgb 0 1 0)'})} + +{GENERATED_FILE_LINE} +# - +""" + +lines.insert(0, REFTEST_LIST_PREAMBLE) +lines.append("") + +# - + +for line in lines: + print(line) + +if "--write" not in ARGS: + eprint("Use --write to write. Exiting...") + sys.exit(0) + +# - + +eprint("Concatenating...") +file_str = "\n".join([line.rstrip() for line in lines]) + +eprint(f"Writing to {DEST}...") +DEST.write_bytes(file_str.encode()) +eprint("Done!") + +sys.exit(0) |