From 6587d6522b2f8097bc95a2008f224421c984b98f Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 19 Jun 2024 13:44:46 -0700 Subject: [PATCH 1/8] tools: Add initial basic texture analysis script In order to calibrate the texture compression settings better, we need a way to analyze a few representative files and plot the dependency between quality/size and compression parameters. This version focuses on UASTC and UASTC RDO lambda specifically as it's the most sensitive parameter. --- tools/bitmask.py | 0 tools/gltfbasis.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) mode change 100755 => 100644 tools/bitmask.py create mode 100644 tools/gltfbasis.py diff --git a/tools/bitmask.py b/tools/bitmask.py old mode 100755 new mode 100644 diff --git a/tools/gltfbasis.py b/tools/gltfbasis.py new file mode 100644 index 00000000..806afa7a --- /dev/null +++ b/tools/gltfbasis.py @@ -0,0 +1,61 @@ +#!/usr/bin/python3 + +import argparse +import matplotlib.pyplot as plt +import os +import os.path +import re +import subprocess + +argp = argparse.ArgumentParser() +argp.add_argument('--basisu', type=str, required=True) +argp.add_argument('files', type=str, nargs='+') +args = argp.parse_args() + +def compress(path, flags): + temp_path = "/dev/null" if os.name == "posix" else "NUL" + output = subprocess.check_output([args.basisu, "-file", path, "-output_file", temp_path, "-stats", "-ktx2"] + flags) + for line in output.splitlines(): + if m := re.match(r".*source image.*?(\d+)x(\d+)", line.decode()): + pixels = int(m.group(1)) * int(m.group(2)) + elif m := re.match(r".*Compression succeeded.*size (\d+) bytes", line.decode()): + bytes = int(m.group(1)) + elif m := re.match(r"\.basis RGB Avg:.*RMS: (\d+\.\d+) PSNR: (\d+\.\d+)", line.decode()): + rms = float(m.group(1)) + psnr = float(m.group(2)) + + return {'path': path, 'bpp': bytes * 8 / pixels, 'rms': rms, 'psnr': psnr} + +def uastc_stats(path): + results = [] + for rdo_l in range(0, 20): + flags = ["-uastc", "-uastc_level", "1", "-uastc_rdo_l", str(rdo_l / 5), "-uastc_rdo_d", "1024"] + res = compress(path, flags) + res['rdo_l'] = rdo_l / 5 + results.append(res) + return results + +plt.figure(figsize=(15, 5)) + +for path in args.files: + print('Processing', path) + results = uastc_stats(path) + name = os.path.basename(path) + plt.subplot(1, 3, 1) + plt.plot([r['rdo_l'] for r in results], [r['bpp'] for r in results], label=name) + plt.subplot(1, 3, 2) + plt.plot([r['rdo_l'] for r in results], [r['rms'] for r in results], label=name) + plt.subplot(1, 3, 3) + plt.plot([r['rdo_l'] for r in results], [r['psnr'] for r in results], label=name) + +plt.subplot(1, 3, 1) +plt.title('bpp') +plt.legend() +plt.subplot(1, 3, 2) +plt.title('rms') +plt.legend() +plt.subplot(1, 3, 3) +plt.title('psnr') +plt.legend() + +plt.savefig('basisu.png') From 989d97577cf8577d57543f3cf806e7d2109ddf53 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 19 Jun 2024 14:13:12 -0700 Subject: [PATCH 2/8] tools: Add ETC1S baseline data to gltfbasis.py Without this it's difficult to judge the relative improvement/regression as the values naturally vary per image. --- tools/gltfbasis.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tools/gltfbasis.py b/tools/gltfbasis.py index 806afa7a..af8bd8dd 100644 --- a/tools/gltfbasis.py +++ b/tools/gltfbasis.py @@ -40,13 +40,17 @@ plt.figure(figsize=(15, 5)) for path in args.files: print('Processing', path) results = uastc_stats(path) + etcbase = compress(path, ["-q", "192"]) name = os.path.basename(path) plt.subplot(1, 3, 1) - plt.plot([r['rdo_l'] for r in results], [r['bpp'] for r in results], label=name) + line, = plt.plot([r['rdo_l'] for r in results], [r['bpp'] for r in results], label=name) + plt.axhline(etcbase['bpp'], color=line.get_color(), linestyle='dotted') plt.subplot(1, 3, 2) - plt.plot([r['rdo_l'] for r in results], [r['rms'] for r in results], label=name) + line, = plt.plot([r['rdo_l'] for r in results], [r['rms'] for r in results], label=name) + plt.axhline(etcbase['rms'], color=line.get_color(), linestyle='dotted') plt.subplot(1, 3, 3) - plt.plot([r['rdo_l'] for r in results], [r['psnr'] for r in results], label=name) + line, = plt.plot([r['rdo_l'] for r in results], [r['psnr'] for r in results], label=name) + plt.axhline(etcbase['psnr'], color=line.get_color(), linestyle='dotted') plt.subplot(1, 3, 1) plt.title('bpp') From 4c7b2d04e0289c54743a0095e2ef858b7c20750d Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 19 Jun 2024 14:54:34 -0700 Subject: [PATCH 3/8] tools: Rework graph plotting and layout in gltfbasis We now use constrained layout to be able to put the combined legend outside of the plot area and switch to using Axes to make the code a little more robust. --- tools/gltfbasis.py | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/tools/gltfbasis.py b/tools/gltfbasis.py index af8bd8dd..03b6d86a 100644 --- a/tools/gltfbasis.py +++ b/tools/gltfbasis.py @@ -26,7 +26,7 @@ def compress(path, flags): return {'path': path, 'bpp': bytes * 8 / pixels, 'rms': rms, 'psnr': psnr} -def uastc_stats(path): +def stats(path): results = [] for rdo_l in range(0, 20): flags = ["-uastc", "-uastc_level", "1", "-uastc_rdo_l", str(rdo_l / 5), "-uastc_rdo_d", "1024"] @@ -35,31 +35,25 @@ def uastc_stats(path): results.append(res) return results -plt.figure(figsize=(15, 5)) +fields = ['bpp', 'rms', 'psnr'] +fig, axs = plt.subplots(1, len(fields), layout='constrained') +fig.set_figwidth(5 * len(fields)) +lines = [] for path in args.files: print('Processing', path) - results = uastc_stats(path) + results = stats(path) etcbase = compress(path, ["-q", "192"]) - name = os.path.basename(path) - plt.subplot(1, 3, 1) - line, = plt.plot([r['rdo_l'] for r in results], [r['bpp'] for r in results], label=name) - plt.axhline(etcbase['bpp'], color=line.get_color(), linestyle='dotted') - plt.subplot(1, 3, 2) - line, = plt.plot([r['rdo_l'] for r in results], [r['rms'] for r in results], label=name) - plt.axhline(etcbase['rms'], color=line.get_color(), linestyle='dotted') - plt.subplot(1, 3, 3) - line, = plt.plot([r['rdo_l'] for r in results], [r['psnr'] for r in results], label=name) - plt.axhline(etcbase['psnr'], color=line.get_color(), linestyle='dotted') -plt.subplot(1, 3, 1) -plt.title('bpp') -plt.legend() -plt.subplot(1, 3, 2) -plt.title('rms') -plt.legend() -plt.subplot(1, 3, 3) -plt.title('psnr') -plt.legend() + for idx, field in enumerate(fields): + line, = axs[idx].plot([r['rdo_l'] for r in results], [r[field] for r in results]) + axs[idx].axhline(etcbase[field], color=line.get_color(), linestyle='dotted') + if idx == 0: + lines.append(line) + +for idx, field in enumerate(fields): + axs[idx].set_title(field) + +fig.legend(lines, [os.path.basename(path) for path in args.files], loc='outside right upper') plt.savefig('basisu.png') From 8177b9140fe1fac444d7b6db4262a9700e5b7858 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 19 Jun 2024 15:05:50 -0700 Subject: [PATCH 4/8] tools: Parallelize texture processing in gltfbasis Use concurrent.futures to compress many texture variants at once, as individual texture processing doesn't saturate more than a couple cores via internal parallelism. --- tools/gltfbasis.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tools/gltfbasis.py b/tools/gltfbasis.py index 03b6d86a..1345d187 100644 --- a/tools/gltfbasis.py +++ b/tools/gltfbasis.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 import argparse +import concurrent.futures import matplotlib.pyplot as plt import os import os.path @@ -27,13 +28,19 @@ def compress(path, flags): return {'path': path, 'bpp': bytes * 8 / pixels, 'rms': rms, 'psnr': psnr} def stats(path): - results = [] - for rdo_l in range(0, 20): - flags = ["-uastc", "-uastc_level", "1", "-uastc_rdo_l", str(rdo_l / 5), "-uastc_rdo_d", "1024"] - res = compress(path, flags) - res['rdo_l'] = rdo_l / 5 - results.append(res) - return results + with concurrent.futures.ThreadPoolExecutor(16) as executor: + futures = [] + for i in range(0, 30): + rdo_l = i / 5 + flags = ["-uastc", "-uastc_level", "1", "-uastc_rdo_l", str(rdo_l), "-uastc_rdo_d", "1024"] + futures.append((executor.submit(compress, path, flags), rdo_l)) + concurrent.futures.wait([f for (f, r) in futures]) + results = [] + for future, rdo_l in futures: + res = future.result() + res['rdo_l'] = rdo_l + results.append(res) + return results fields = ['bpp', 'rms', 'psnr'] fig, axs = plt.subplots(1, len(fields), layout='constrained') From 7b7f59f210369234882a47d64e2311bd75686da1 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Thu, 20 Jun 2024 18:24:49 -0700 Subject: [PATCH 5/8] tools: Add graph path argument to gltfbasis Also change the RDO lambda range to 0..5 --- tools/gltfbasis.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/gltfbasis.py b/tools/gltfbasis.py index 1345d187..d9629837 100644 --- a/tools/gltfbasis.py +++ b/tools/gltfbasis.py @@ -10,6 +10,7 @@ import subprocess argp = argparse.ArgumentParser() argp.add_argument('--basisu', type=str, required=True) +argp.add_argument('--graph', type=str, default="basisu.png") argp.add_argument('files', type=str, nargs='+') args = argp.parse_args() @@ -30,7 +31,7 @@ def compress(path, flags): def stats(path): with concurrent.futures.ThreadPoolExecutor(16) as executor: futures = [] - for i in range(0, 30): + for i in range(0, 26): rdo_l = i / 5 flags = ["-uastc", "-uastc_level", "1", "-uastc_rdo_l", str(rdo_l), "-uastc_rdo_d", "1024"] futures.append((executor.submit(compress, path, flags), rdo_l)) @@ -63,4 +64,4 @@ for idx, field in enumerate(fields): fig.legend(lines, [os.path.basename(path) for path in args.files], loc='outside right upper') -plt.savefig('basisu.png') +plt.savefig(args.graph) From c8d0b4903ff854335d13a2c114c27f959f66f844 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Fri, 21 Jun 2024 10:08:32 -0700 Subject: [PATCH 6/8] tools: Add compression ratio and PSNR scatter plot to gltfbasis We now plot compression ratio in addition to bpp which is easier to read because it normalizes the RDO size impact, and a scatter plot of PSNR to ratio that allows to understand the trend lines for different textures better. --- tools/gltfbasis.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tools/gltfbasis.py b/tools/gltfbasis.py index d9629837..cde9b030 100644 --- a/tools/gltfbasis.py +++ b/tools/gltfbasis.py @@ -37,15 +37,19 @@ def stats(path): futures.append((executor.submit(compress, path, flags), rdo_l)) concurrent.futures.wait([f for (f, r) in futures]) results = [] + bppbase = 0 for future, rdo_l in futures: res = future.result() + if rdo_l == 0: + bppbase = res['bpp'] res['rdo_l'] = rdo_l + res['ratio'] = res['bpp'] / bppbase results.append(res) return results -fields = ['bpp', 'rms', 'psnr'] -fig, axs = plt.subplots(1, len(fields), layout='constrained') -fig.set_figwidth(5 * len(fields)) +fields = ['bpp', 'rms', 'psnr', 'ratio'] +fig, axs = plt.subplots(1, len(fields) + 1, layout='constrained') +fig.set_figwidth(5 * (len(fields) + 1)) lines = [] for path in args.files: @@ -55,13 +59,18 @@ for path in args.files: for idx, field in enumerate(fields): line, = axs[idx].plot([r['rdo_l'] for r in results], [r[field] for r in results]) - axs[idx].axhline(etcbase[field], color=line.get_color(), linestyle='dotted') + if field in etcbase: + axs[idx].axhline(etcbase[field], color=line.get_color(), linestyle='dotted') if idx == 0: lines.append(line) + axs[len(fields)].scatter([r['ratio'] for r in results], [r['psnr'] for r in results], color=line.get_color()) + for idx, field in enumerate(fields): axs[idx].set_title(field) +axs[len(fields)].set_title('psnr vs ratio') + fig.legend(lines, [os.path.basename(path) for path in args.files], loc='outside right upper') plt.savefig(args.graph) From 49415c90600afa00bc2af2ec9f932741156c36af Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Fri, 21 Jun 2024 10:57:58 -0700 Subject: [PATCH 7/8] tools: Make ETC baseline opt in in gltfbasis These baselines are very helpful to analyze a small set of files but very distracting on a large set of files, so make this opt in via a command line argument. --- tools/gltfbasis.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/gltfbasis.py b/tools/gltfbasis.py index cde9b030..9fc88166 100644 --- a/tools/gltfbasis.py +++ b/tools/gltfbasis.py @@ -11,6 +11,7 @@ import subprocess argp = argparse.ArgumentParser() argp.add_argument('--basisu', type=str, required=True) argp.add_argument('--graph', type=str, default="basisu.png") +argp.add_argument('--etcbase', action='store_true') argp.add_argument('files', type=str, nargs='+') args = argp.parse_args() @@ -55,11 +56,11 @@ lines = [] for path in args.files: print('Processing', path) results = stats(path) - etcbase = compress(path, ["-q", "192"]) + etcbase = compress(path, ["-q", "192"]) if args.etcbase else None for idx, field in enumerate(fields): line, = axs[idx].plot([r['rdo_l'] for r in results], [r[field] for r in results]) - if field in etcbase: + if etcbase and field in etcbase: axs[idx].axhline(etcbase[field], color=line.get_color(), linestyle='dotted') if idx == 0: lines.append(line) From f0fb6657a77ab0d9f7f2056f75972aba3aa7afba Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Fri, 21 Jun 2024 11:21:29 -0700 Subject: [PATCH 8/8] gltfpack: Rebalance texture quality table The main goal of this change is to tweak the UASTC RDO curve. The initial values here have been picked for early UASTC versions that used a different RDO algorithm, and haven't been revisited since. The new table makes it so that at tq 5, we use the same defaults as basisu (q=128 for ETC1 and l=1 for UASTC RDO), and tweak the table to be in general a little more aggressive for UASTC. We use different curves before l=1 and after as the expectation is that for earlier quality levels, the size is much more important than quality. The default lambda for tq=8 is a little larger now (0.4 vs 0.3) which will result in a small size improvement and still good quality by default. There's maybe a little more room for default lambda to be higher, but eg lambda=1 does not seem practical for normal maps on a range of production assets, so we keep the curve for tq=6..10 to be more quality focused. The new table is not based on a rigorous fitting of the actual quality/size curves, but the new tool gltfbasis.py can help find a more optimal table in the future. --- gltf/basisenc.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/gltf/basisenc.cpp b/gltf/basisenc.cpp index c1080949..c415d2c5 100644 --- a/gltf/basisenc.cpp +++ b/gltf/basisenc.cpp @@ -26,15 +26,15 @@ struct BasisSettings }; static const BasisSettings kBasisSettings[10] = { - {1, 1, 0, 1.5f}, - {1, 6, 0, 1.f}, - {1, 20, 1, 1.0f}, - {1, 50, 1, 0.75f}, - {1, 90, 1, 0.5f}, - {1, 128, 1, 0.4f}, - {1, 160, 1, 0.34f}, - {1, 192, 1, 0.29f}, // default - {1, 224, 2, 0.26f}, + {1, 1, 0, 4.f}, + {1, 32, 0, 3.f}, + {1, 64, 1, 2.f}, + {1, 96, 1, 1.5f}, + {1, 128, 1, 1.f}, // quality arguments aligned with basisu defaults + {1, 150, 1, 0.8f}, + {1, 170, 1, 0.6f}, + {1, 192, 1, 0.4f}, // gltfpack defaults + {1, 224, 2, 0.2f}, {1, 255, 2, 0.f}, };