SkQP: fix model colorspace (16-bit gold images)
Problem: `make_gmkb.go `was ignoring the ignoring the embedded ICC profile in the images it was getting from Gold. Replace make_gmkb.go with two small programs: `goldgetter.py` and `make_skqp_model.cpp`. `make_skqp_model` uses Skia to create the model from a bunch of images. `goldgetter` wraps `make_skqp_model` and handles: - json parsing - downloading images from gold - multiprocessing CQ_INCLUDE_TRYBOTS=skia.primary:Build-Debian9-Clang-x86-devrel-Android_SKQP,Test-Debian9-Clang-NUC7i5BNK-CPU-Emulator-x86-devrel-All-Android_SKQP Change-Id: I7add1a1dfd83bbd0ab07ab126d4183c36325263c Reviewed-on: https://skia-review.googlesource.com/c/skia/+/209101 Commit-Queue: Hal Canary <halcanary@google.com> Reviewed-by: Mike Klein <mtklein@google.com>
This commit is contained in:
parent
e6bfb7daf0
commit
5b39dc8153
9
BUILD.gn
9
BUILD.gn
@ -1812,6 +1812,15 @@ if (skia_enable_tools) {
|
||||
}
|
||||
}
|
||||
|
||||
test_app("make_skqp_model") {
|
||||
sources = [
|
||||
"tools/skqp/make_skqp_model.cpp",
|
||||
]
|
||||
deps = [
|
||||
":skia",
|
||||
]
|
||||
}
|
||||
|
||||
if (target_cpu != "wasm") {
|
||||
import("gn/samples.gni")
|
||||
test_lib("samples") {
|
||||
|
@ -11,21 +11,16 @@ set -x
|
||||
set -e
|
||||
META_JSON="$1"
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
if [ -z "$SKQP_SKIP_INFRA_UPDATE" ]; then
|
||||
go get -u go.skia.org/infra/golden/go/search
|
||||
fi
|
||||
go run tools/skqp/make_gmkb.go \
|
||||
"$META_JSON" \
|
||||
platform_tools/android/apps/skqp/src/main/assets/gmkb
|
||||
env GIT_SYNC_DEPS_QUIET=1 python tools/git-sync-deps
|
||||
O='out/ndebug'
|
||||
mkdir -p $O
|
||||
bin/gn gen $O --args='cc="clang" cxx="clang++" is_debug=false'
|
||||
ninja -C $O jitter_gms list_gpu_unit_tests
|
||||
ninja -C $O jitter_gms list_gpu_unit_tests make_skqp_model
|
||||
GMKB='platform_tools/android/apps/skqp/src/main/assets/gmkb'
|
||||
python tools/skqp/goldgetter.py "$META_JSON" "$GMKB" $O/make_skqp_model
|
||||
$O/jitter_gms tools/skqp/bad_gms.txt
|
||||
python tools/skqp/make_rendertests_list.py
|
||||
rm 'bad.txt' 'good.txt'
|
||||
rm 'bad.txt' 'good.txt' "$GMKB"/models.txt
|
||||
sh tools/skqp/upload_model
|
||||
$O/list_gpu_unit_tests \
|
||||
> platform_tools/android/apps/skqp/src/main/assets/skqp/unittests.txt
|
||||
|
48
tools/skqp/goldgetter.py
Executable file
48
tools/skqp/goldgetter.py
Executable file
@ -0,0 +1,48 @@
|
||||
#! /usr/bin/env python
|
||||
# Copyright 2019 Google LLC.
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
import json
|
||||
import multiprocessing
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import urllib
|
||||
|
||||
def make_skqp_model(arg):
|
||||
name, urls, dst_dir, exe = arg
|
||||
tmp = tempfile.mkdtemp()
|
||||
for url in urls:
|
||||
urllib.urlretrieve(url, tmp + '/' + url[url.rindex('/') + 1:])
|
||||
subprocess.check_call([exe, tmp, dst_dir + '/' + name])
|
||||
shutil.rmtree(tmp)
|
||||
sys.stdout.write(name + ' ')
|
||||
sys.stdout.flush()
|
||||
|
||||
def main(meta, dst, exe):
|
||||
assert os.path.exists(exe)
|
||||
jobs = []
|
||||
with open(meta, 'r') as f:
|
||||
for rec in json.load(f):
|
||||
urls = [d['URL'] for d in rec['digests']
|
||||
if d['status'] == 'positive' and
|
||||
(set(d['paramset']['config']) & set(['vk', 'gles']))]
|
||||
if urls:
|
||||
jobs.append((rec['testName'], urls, dst, exe))
|
||||
if not os.path.exists(dst):
|
||||
os.mkdir(dst)
|
||||
pool = multiprocessing.Pool(processes=20)
|
||||
pool.map(make_skqp_model, jobs)
|
||||
sys.stdout.write('\n')
|
||||
with open(dst + '/models.txt', 'w') as o:
|
||||
for n, _, _, _ in jobs:
|
||||
o.write(n + '\n')
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 4:
|
||||
sys.stderr.write('Usage:\n %s META.JSON DST_DIR MAKE_SKQP_MODEL_EXE\n\n' % sys.argv[0])
|
||||
sys.exit(1)
|
||||
main(sys.argv[1], sys.argv[2], sys.argv[3])
|
@ -1,213 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Google Inc.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license that can be
|
||||
* found in the LICENSE file.
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"go.skia.org/infra/golden/go/search"
|
||||
)
|
||||
|
||||
const (
|
||||
min_png = "min.png"
|
||||
max_png = "max.png"
|
||||
)
|
||||
|
||||
type ExportTestRecordArray []search.ExportTestRecord
|
||||
|
||||
func (a ExportTestRecordArray) Len() int { return len(a) }
|
||||
func (a ExportTestRecordArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ExportTestRecordArray) Less(i, j int) bool { return a[i].TestName < a[j].TestName }
|
||||
|
||||
func in(v string, a []string) bool {
|
||||
for _, u := range a {
|
||||
if u == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func clampU8(v int) uint8 {
|
||||
if v < 0 {
|
||||
return 0
|
||||
} else if v > 255 {
|
||||
return 255
|
||||
}
|
||||
return uint8(v)
|
||||
}
|
||||
|
||||
func processTest(testName string, imgUrls []string, output string) (bool, error) {
|
||||
if strings.ContainsRune(testName, '/') {
|
||||
return false, nil
|
||||
}
|
||||
output_directory := path.Join(output, testName)
|
||||
var img_max image.NRGBA
|
||||
var img_min image.NRGBA
|
||||
for _, url := range imgUrls {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
img, err := png.Decode(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if img_max.Rect.Max.X == 0 {
|
||||
// N.B. img_max.Pix may alias img.Pix (if they're already NRGBA).
|
||||
img_max = toNrgba(img)
|
||||
img_min = copyNrgba(img_max)
|
||||
continue
|
||||
}
|
||||
w := img.Bounds().Max.X - img.Bounds().Min.X
|
||||
h := img.Bounds().Max.Y - img.Bounds().Min.Y
|
||||
if img_max.Rect.Max.X != w || img_max.Rect.Max.Y != h {
|
||||
return false, errors.New("size mismatch")
|
||||
}
|
||||
img_nrgba := toNrgba(img)
|
||||
for i, value := range img_nrgba.Pix {
|
||||
if value > img_max.Pix[i] {
|
||||
img_max.Pix[i] = value
|
||||
} else if value < img_min.Pix[i] {
|
||||
img_min.Pix[i] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
if img_max.Rect.Max.X == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := os.Mkdir(output_directory, os.ModePerm); err != nil && !os.IsExist(err) {
|
||||
return false, err
|
||||
}
|
||||
if err := writePngToFile(path.Join(output_directory, min_png), &img_min); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := writePngToFile(path.Join(output_directory, max_png), &img_max); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type LockedStringList struct {
|
||||
List []string
|
||||
mux sync.Mutex
|
||||
}
|
||||
|
||||
func (l *LockedStringList) add(v string) {
|
||||
l.mux.Lock()
|
||||
defer l.mux.Unlock()
|
||||
l.List = append(l.List, v)
|
||||
}
|
||||
|
||||
|
||||
func readMetaJsonFile(filename string) ([]search.ExportTestRecord, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dec := json.NewDecoder(file)
|
||||
var records []search.ExportTestRecord
|
||||
err = dec.Decode(&records)
|
||||
return records, err
|
||||
}
|
||||
|
||||
func writePngToFile(path string, img image.Image) error {
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
return png.Encode(file, img)
|
||||
}
|
||||
|
||||
// to_nrgb() may return a shallow copy of img if it's already NRGBA.
|
||||
func toNrgba(img image.Image) image.NRGBA {
|
||||
switch v := img.(type) {
|
||||
case *image.NRGBA:
|
||||
return *v
|
||||
}
|
||||
nimg := *image.NewNRGBA(img.Bounds())
|
||||
draw.Draw(&nimg, img.Bounds(), img, image.Point{0, 0}, draw.Src)
|
||||
return nimg
|
||||
}
|
||||
|
||||
func copyNrgba(src image.NRGBA) image.NRGBA {
|
||||
dst := image.NRGBA{make([]uint8, len(src.Pix)), src.Stride, src.Rect}
|
||||
copy(dst.Pix, src.Pix)
|
||||
return dst
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 3 {
|
||||
log.Printf("Usage:\n %s INPUT.json OUTPUT_DIRECTORY\n\n", os.Args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
input := os.Args[1]
|
||||
output := os.Args[2]
|
||||
// output is removed and replaced with a clean directory.
|
||||
if err := os.RemoveAll(output); err != nil && !os.IsNotExist(err) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(output, os.ModePerm); err != nil && !os.IsExist(err) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
records, err := readMetaJsonFile(input)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
sort.Sort(ExportTestRecordArray(records))
|
||||
|
||||
var results LockedStringList
|
||||
var wg sync.WaitGroup
|
||||
for _, record := range records {
|
||||
var goodUrls []string
|
||||
for _, digest := range record.Digests {
|
||||
if (in("vk", digest.ParamSet["config"]) ||
|
||||
in("gles", digest.ParamSet["config"])) &&
|
||||
digest.Status == "positive" {
|
||||
goodUrls = append(goodUrls, digest.URL)
|
||||
}
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(testName string, imgUrls []string, output string, results* LockedStringList) {
|
||||
defer wg.Done()
|
||||
success, err := processTest(testName, imgUrls, output)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if success {
|
||||
results.add(testName)
|
||||
}
|
||||
fmt.Printf("\r%-60s", testName)
|
||||
}(record.TestName, goodUrls, output, &results)
|
||||
}
|
||||
wg.Wait()
|
||||
fmt.Printf("\r%60s\n", "")
|
||||
sort.Strings(results.List)
|
||||
modelFile, err := os.Create(path.Join(output, "models.txt"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, v := range results.List {
|
||||
fmt.Fprintln(modelFile, v)
|
||||
}
|
||||
}
|
82
tools/skqp/make_skqp_model.cpp
Normal file
82
tools/skqp/make_skqp_model.cpp
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright 2019 Google LLC.
|
||||
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
#include "SkBitmap.h"
|
||||
#include "SkCodec.h"
|
||||
#include "SkPngEncoder.h"
|
||||
#include "SkOSFile.h"
|
||||
|
||||
static void update(SkBitmap* maxBitmap, SkBitmap* minBitmap, const SkBitmap& bm) {
|
||||
SkASSERT(!bm.drawsNothing());
|
||||
SkASSERT(4 == bm.bytesPerPixel());
|
||||
if (maxBitmap->drawsNothing()) {
|
||||
maxBitmap->allocPixels(bm.info());
|
||||
maxBitmap->eraseColor(0x00000000);
|
||||
minBitmap->allocPixels(bm.info());
|
||||
minBitmap->eraseColor(0xFFFFFFFF);
|
||||
}
|
||||
SkASSERT_RELEASE(maxBitmap->info() == bm.info());
|
||||
const SkPixmap& pmin = minBitmap->pixmap();
|
||||
const SkPixmap& pmax = maxBitmap->pixmap();
|
||||
const SkPixmap& pm = bm.pixmap();
|
||||
for (int y = 0; y < pm.height(); ++y) {
|
||||
for (int x = 0; x < pm.width(); ++x) {
|
||||
uint32_t* minPtr = pmin.writable_addr32(x, y);
|
||||
uint32_t* maxPtr = pmax.writable_addr32(x, y);
|
||||
uint8_t minColor[4], maxColor[4], color[4];
|
||||
memcpy(minColor, minPtr, 4);
|
||||
memcpy(maxColor, maxPtr, 4);
|
||||
memcpy(color, pm.addr32(x, y), 4);
|
||||
for (unsigned i = 0; i < 4; ++i) {
|
||||
minColor[i] = std::min(minColor[i], color[i]);
|
||||
maxColor[i] = std::max(maxColor[i], color[i]);
|
||||
}
|
||||
memcpy(minPtr, minColor, 4);
|
||||
memcpy(maxPtr, maxColor, 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static SkBitmap decode_to_srgb_8888_unpremul(const char* path) {
|
||||
SkBitmap dst;
|
||||
if (auto codec = SkCodec::MakeFromData(SkData::MakeFromFileName(path))) {
|
||||
SkISize size = codec->getInfo().dimensions();
|
||||
SkASSERT(!size.isEmpty());
|
||||
dst.allocPixels(SkImageInfo::Make(
|
||||
size.width(), size.height(), kRGBA_8888_SkColorType,
|
||||
kUnpremul_SkAlphaType, SkColorSpace::MakeSRGB()));
|
||||
if (SkCodec::kSuccess != codec->getPixels(dst.pixmap())) {
|
||||
dst.reset();
|
||||
}
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
bool encode_png(const char* path, const SkPixmap& pixmap) {
|
||||
if (!pixmap.addr()) {
|
||||
return false;
|
||||
}
|
||||
SkPngEncoder::Options encOpts;
|
||||
encOpts.fZLibLevel = 9; // slow encode;
|
||||
SkFILEWStream o(path);
|
||||
return o.isValid() && SkPngEncoder::Encode(&o, pixmap, encOpts);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
SkASSERT_RELEASE(argc > 2);
|
||||
const char* src_dir = argv[1];
|
||||
const char* dst_dir = argv[2];
|
||||
SkBitmap maxBitmap, minBitmap;
|
||||
SkOSFile::Iter iter(src_dir);
|
||||
SkString name;
|
||||
while (iter.next(&name)) {
|
||||
name.prependf("%s/", src_dir);
|
||||
SkBitmap bm = decode_to_srgb_8888_unpremul(name.c_str());
|
||||
if (!bm.drawsNothing()) {
|
||||
update(&maxBitmap, &minBitmap, bm);
|
||||
}
|
||||
}
|
||||
SkASSERT_RELEASE(sk_mkdir(dst_dir));
|
||||
encode_png(SkStringPrintf("%s/min.png", dst_dir).c_str(), minBitmap.pixmap());
|
||||
encode_png(SkStringPrintf("%s/max.png", dst_dir).c_str(), maxBitmap.pixmap());
|
||||
}
|
@ -34,7 +34,8 @@ fi
|
||||
|
||||
TDIR="$(mktemp -d "${TMPDIR:-/tmp}/skqp_report.XXXXXXXXXX")"
|
||||
|
||||
adb install -r "$APK" || exit 2
|
||||
adb uninstall org.skia.skqp
|
||||
adb install "$APK" || exit 2
|
||||
adb logcat -c
|
||||
|
||||
adb logcat TestRunner org.skia.skqp skia DEBUG "*:S" | tee "${TDIR}/logcat.txt" &
|
||||
@ -72,7 +73,7 @@ REPORT="${TDIR}/${odir_basename}/report.html"
|
||||
if [ -f "$REPORT" ]; then
|
||||
grep 'f(.*;' "$REPORT"
|
||||
echo "$REPORT"
|
||||
"$(dirname "$0")"/../../bin/sysopen "$REPORT"
|
||||
"$(dirname "$0")"/../../bin/sysopen "$REPORT" > /dev/null 2>&1 &
|
||||
else
|
||||
echo "$TDIR"
|
||||
fi
|
||||
|
Loading…
Reference in New Issue
Block a user