xref: /aosp_15_r20/external/skia/modules/canvaskit/gm_bindings.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1 /*
2  * Copyright 2020 Google LLC
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 
8 #include <set>
9 #include <string>
10 #include <emscripten.h>
11 #include <emscripten/bind.h>
12 #include <emscripten/html5.h>
13 
14 #include "gm/gm.h"
15 #include "include/core/SkBitmap.h"
16 #include "include/core/SkCanvas.h"
17 #include "include/core/SkData.h"
18 #include "include/core/SkImageInfo.h"
19 #include "include/core/SkStream.h"
20 #include "include/core/SkSurface.h"
21 #include "include/gpu/ganesh/GrContextOptions.h"
22 #include "include/gpu/ganesh/GrDirectContext.h"
23 #include "include/gpu/ganesh/SkSurfaceGanesh.h"
24 #include "include/gpu/ganesh/gl/GrGLDirectContext.h"
25 #include "include/gpu/ganesh/gl/GrGLInterface.h"
26 #include "include/gpu/ganesh/gl/GrGLTypes.h"
27 #include "modules/canvaskit/WasmCommon.h"
28 #include "src/core/SkMD5.h"
29 #include "tests/Test.h"
30 #include "tests/TestHarness.h"
31 #include "tools/HashAndEncode.h"
32 #include "tools/ResourceFactory.h"
33 #include "tools/flags/CommandLineFlags.h"
34 #include "tools/fonts/FontToolUtils.h"
35 #include "tools/gpu/ContextType.h"
36 
37 using namespace emscripten;
38 
39 /**
40  * Returns a JS array of strings containing the names of the registered GMs. GMs are only registered
41  * when their source is included in the "link" step, not if they are in something like libgm.a.
42  * The names are also logged to the console.
43  */
ListGMs()44 static JSArray ListGMs() {
45     SkDebugf("Listing GMs\n");
46     JSArray gms = emscripten::val::array();
47     for (const skiagm::GMFactory& fact : skiagm::GMRegistry::Range()) {
48         std::unique_ptr<skiagm::GM> gm(fact());
49         SkDebugf("gm %s\n", gm->getName().c_str());
50         gms.call<void>("push", std::string(gm->getName().c_str()));
51     }
52     return gms;
53 }
54 
getGMWithName(std::string name)55 static std::unique_ptr<skiagm::GM> getGMWithName(std::string name) {
56     for (const skiagm::GMFactory& fact : skiagm::GMRegistry::Range()) {
57         std::unique_ptr<skiagm::GM> gm(fact());
58         if (gm->getName().c_str() == name) {
59             return gm;
60         }
61     }
62     return nullptr;
63 }
64 
65 /**
66  * Sets the given WebGL context to be "current" and then creates a GrDirectContext from that
67  * context.
68  */
MakeGrContext(EMSCRIPTEN_WEBGL_CONTEXT_HANDLE context)69 static sk_sp<GrDirectContext> MakeGrContext(EMSCRIPTEN_WEBGL_CONTEXT_HANDLE context)
70 {
71     EMSCRIPTEN_RESULT r = emscripten_webgl_make_context_current(context);
72     if (r < 0) {
73         printf("failed to make webgl context current %d\n", r);
74         return nullptr;
75     }
76     // setup GrDirectContext
77     auto interface = GrGLMakeNativeInterface();
78     // setup contexts
79     sk_sp<GrDirectContext> dContext((GrDirectContexts::MakeGL(interface)));
80     return dContext;
81 }
82 
83 static std::set<std::string> gKnownDigests;
84 
LoadKnownDigest(std::string md5)85 static void LoadKnownDigest(std::string md5) {
86   gKnownDigests.insert(md5);
87 }
88 
89 static std::map<std::string, sk_sp<SkData>> gResources;
90 
getResource(const char * name)91 static sk_sp<SkData> getResource(const char* name) {
92   auto it = gResources.find(name);
93   if (it == gResources.end()) {
94     SkDebugf("Resource %s not found\n", name);
95     return nullptr;
96   }
97   return it->second;
98 }
99 
LoadResource(std::string name,WASMPointerU8 bPtr,size_t len)100 static void LoadResource(std::string name, WASMPointerU8 bPtr, size_t len) {
101   const uint8_t* bytes = reinterpret_cast<const uint8_t*>(bPtr);
102   auto data = SkData::MakeFromMalloc(bytes, len);
103   gResources[name] = std::move(data);
104 
105   if (!gResourceFactory) {
106     gResourceFactory = getResource;
107   }
108 }
109 
110 /**
111  * Runs the given GM and returns a JS object. If the GM was successful, the object will have the
112  * following properties:
113  *   "png" - a Uint8Array of the PNG data extracted from the surface.
114  *   "hash" - a string which is the md5 hash of the pixel contents and the metadata.
115  */
RunGM(sk_sp<GrDirectContext> ctx,std::string name)116 static JSObject RunGM(sk_sp<GrDirectContext> ctx, std::string name) {
117     JSObject result = emscripten::val::object();
118     auto gm = getGMWithName(name);
119     if (!gm) {
120         SkDebugf("Could not find gm with name %s\n", name.c_str());
121         return result;
122     }
123     // TODO(kjlubick) make these configurable somehow. This probably makes sense to do as function
124     //   parameters.
125     auto alphaType = SkAlphaType::kPremul_SkAlphaType;
126     auto colorType = SkColorType::kN32_SkColorType;
127     SkISize size = gm->getISize();
128     SkImageInfo info = SkImageInfo::Make(size, colorType, alphaType);
129     sk_sp<SkSurface> surface(SkSurfaces::RenderTarget(
130             ctx.get(), skgpu::Budgeted::kYes, info, 0, kBottomLeft_GrSurfaceOrigin, nullptr, true));
131     if (!surface) {
132         SkDebugf("Could not make surface\n");
133         return result;
134     }
135     auto canvas = surface->getCanvas();
136 
137     gm->onceBeforeDraw();
138     SkString msg;
139     // Based on GMSrc::draw from DM.
140     auto gpuSetupResult = gm->gpuSetup(canvas, &msg);
141     if (gpuSetupResult == skiagm::DrawResult::kFail) {
142         SkDebugf("Error with gpu setup for gm %s: %s\n", name.c_str(), msg.c_str());
143         return result;
144     } else if (gpuSetupResult == skiagm::DrawResult::kSkip) {
145         return result;
146     }
147 
148     auto drawResult = gm->draw(canvas, &msg);
149     if (drawResult == skiagm::DrawResult::kFail) {
150         SkDebugf("Error with gm %s: %s\n", name.c_str(), msg.c_str());
151         return result;
152     } else if (drawResult == skiagm::DrawResult::kSkip) {
153         return result;
154     }
155     ctx->flushAndSubmit(surface.get(), GrSyncCpu::kYes);
156 
157     // Based on GPUSink::readBack
158     SkBitmap bitmap;
159     bitmap.allocPixels(info);
160     if (!canvas->readPixels(bitmap, 0, 0)) {
161         SkDebugf("Could not read pixels back\n");
162         return result;
163     }
164 
165     // Now we need to encode to PNG and get the md5 hash of the pixels (and colorspace and stuff).
166     // This is based on Task::Run from DM.cpp
167     std::unique_ptr<HashAndEncode> hashAndEncode = std::make_unique<HashAndEncode>(bitmap);
168     SkString md5;
169     SkMD5 hash;
170     hashAndEncode->feedHash(&hash);
171     SkMD5::Digest digest = hash.finish();
172     for (int i = 0; i < 16; i++) {
173         md5.appendf("%02x", digest.data[i]);
174     }
175 
176     auto ok = gKnownDigests.find(md5.c_str());
177     if (ok == gKnownDigests.end()) {
178         // We only need to decode the image if it is "interesting", that is, we have not written it
179         // before to disk and uploaded it to gold.
180         SkDynamicMemoryWStream stream;
181         // We do not need to include the keys because they are optional - they are not read by Gold.
182         CommandLineFlags::StringArray empty;
183         hashAndEncode->encodePNG(&stream, md5.c_str(), empty, empty);
184 
185         auto data = stream.detachAsData();
186 
187         // This is the cleanest way to create a new Uint8Array with a copy of the data that is not
188         // in the WASM heap. kjlubick tried returning a pointer inside an SkData, but that lead to
189         // some use after free issues. By making the copy using the JS transliteration, we don't
190         // risk the SkData object being cleaned up before we make the copy.
191         Uint8Array pngData = emscripten::val(
192             // https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#memory-views
193             typed_memory_view(data->size(), data->bytes())
194         ).call<Uint8Array>("slice"); // slice with no args makes a copy of the memory view.
195 
196         result.set("png", pngData);
197         gKnownDigests.emplace(md5.c_str());
198     }
199     result.set("hash", md5.c_str());
200     return result;
201 }
202 
ListTests()203 static JSArray ListTests() {
204     SkDebugf("Listing Tests\n");
205     JSArray tests = emscripten::val::array();
206     for (auto test : skiatest::TestRegistry::Range()) {
207         SkDebugf("test %s\n", test.fName);
208         tests.call<void>("push", std::string(test.fName));
209     }
210     return tests;
211 }
212 
getTestWithName(std::string name,bool * ok)213 static skiatest::Test getTestWithName(std::string name, bool* ok) {
214     for (auto test : skiatest::TestRegistry::Range()) {
215         if (name == test.fName) {
216           *ok = true;
217           return test;
218         }
219     }
220     *ok = false;
221     return skiatest::Test::MakeCPU(nullptr, nullptr);
222 }
223 
224 // Based on DM.cpp:run_test
225 struct WasmReporter : public skiatest::Reporter {
WasmReporterWasmReporter226     WasmReporter(std::string name, JSObject result): fName(name), fResult(result){}
227 
reportFailedWasmReporter228     void reportFailed(const skiatest::Failure& failure) override {
229         SkDebugf("Test %s failed: %s\n", fName.c_str(), failure.toString().c_str());
230         fResult.set("result", "failed");
231         fResult.set("msg", failure.toString().c_str());
232     }
233     std::string fName;
234     JSObject fResult;
235 };
236 
237 /**
238  * Runs the given Test and returns a JS object. If the Test was located, the object will have the
239  * following properties:
240  *   "result" : One of "passed", "failed", "skipped".
241  *   "msg": May be non-empty on failure
242  */
RunTest(std::string name)243 static JSObject RunTest(std::string name) {
244     JSObject result = emscripten::val::object();
245     bool ok = false;
246     auto test = getTestWithName(name, &ok);
247     if (!ok) {
248         SkDebugf("Could not find test with name %s\n", name.c_str());
249         return result;
250     }
251     GrContextOptions grOpts;
252     if (test.fTestType == skiatest::TestType::kGanesh) {
253         result.set("result", "passed"); // default to passing - the reporter will mark failed.
254         WasmReporter reporter(name, result);
255         test.modifyGrContextOptions(&grOpts);
256         test.ganesh(&reporter, grOpts);
257         return result;
258     } else if (test.fTestType == skiatest::TestType::kGraphite) {
259         SkDebugf("Graphite test %s not yet supported\n", name.c_str());
260         return result;
261     }
262 
263     result.set("result", "passed"); // default to passing - the reporter will mark failed.
264     WasmReporter reporter(name, result);
265     test.cpu(&reporter);
266     return result;
267 }
268 
269 namespace skiatest {
270 
271 using ContextType = skgpu::ContextType;
272 
273 // These are the supported ContextTypeFilterFn. They are defined in Test.h and implemented here.
IsGLContextType(skgpu::ContextType ct)274 bool IsGLContextType(skgpu::ContextType ct) {
275     return skgpu::ganesh::ContextTypeBackend(ct) == GrBackendApi::kOpenGL;
276 }
IsMockContextType(skgpu::ContextType ct)277 bool IsMockContextType(skgpu::ContextType ct) {
278     return ct == skgpu::ContextType::kMock;
279 }
280 // These are not supported
IsVulkanContextType(ContextType)281 bool IsVulkanContextType(ContextType) { return false; }
IsMetalContextType(ContextType)282 bool IsMetalContextType(ContextType) { return false; }
IsDirect3DContextType(ContextType)283 bool IsDirect3DContextType(ContextType) { return false; }
IsDawnContextType(ContextType)284 bool IsDawnContextType(ContextType) { return false; }
285 
RunWithGaneshTestContexts(GrContextTestFn * testFn,ContextTypeFilterFn * filter,Reporter * reporter,const GrContextOptions & options)286 void RunWithGaneshTestContexts(GrContextTestFn* testFn, ContextTypeFilterFn* filter,
287                                Reporter* reporter, const GrContextOptions& options) {
288     for (auto contextType : {skgpu::ContextType::kGLES, skgpu::ContextType::kMock}) {
289         if (filter && !(*filter)(contextType)) {
290             continue;
291         }
292 
293         sk_gpu_test::GrContextFactory factory(options);
294         sk_gpu_test::ContextInfo ctxInfo = factory.getContextInfo(contextType);
295 
296         REPORTER_ASSERT(reporter, ctxInfo.directContext() != nullptr);
297         if (!ctxInfo.directContext()) {
298             return;
299         }
300         ctxInfo.testContext()->makeCurrent();
301         // From DMGpuTestProcs.cpp
302         (*testFn)(reporter, ctxInfo);
303         // Sync so any release/finished procs get called.
304         ctxInfo.directContext()->flushAndSubmit(GrSyncCpu::kYes);
305     }
306 }
307 } // namespace skiatest
308 
309 namespace {
310 
311 // A GLtestContext that we can return from CreatePlatformGLTestContext below.
312 // It doesn't have to do anything WebGL-specific that I know of but we can't return
313 // a GLTestContext because it has pure virtual methods that need to be implemented.
314 class WasmWebGlTestContext : public sk_gpu_test::GLTestContext {
315 public:
WasmWebGlTestContext()316     WasmWebGlTestContext() {}
~WasmWebGlTestContext()317     ~WasmWebGlTestContext() override {
318         this->teardown();
319     }
320     // We assume WebGL only has one context and that it is always current.
321     // Therefore these context related functions return null intentionally.
322     // It's possible that more tests will pass if these were correctly implemented.
makeNew() const323     std::unique_ptr<GLTestContext> makeNew() const override {
324         // This is supposed to create a new GL context in a new GLTestContext.
325         // Specifically for tests that do not want to re-use the existing one.
326         return nullptr;
327     }
onPlatformMakeNotCurrent() const328     void onPlatformMakeNotCurrent() const override { }
onPlatformMakeCurrent() const329     void onPlatformMakeCurrent() const override { }
onPlatformGetAutoContextRestore() const330     std::function<void()> onPlatformGetAutoContextRestore() const override {
331         return nullptr;
332     }
onPlatformGetProcAddress(const char * procName) const333     GrGLFuncPtr onPlatformGetProcAddress(const char* procName) const override {
334         return nullptr;
335     }
336 };
337 } // namespace
338 
339 namespace sk_gpu_test {
CreatePlatformGLTestContext(GrGLStandard forcedGpuAPI,GLTestContext * shareContext)340 GLTestContext *CreatePlatformGLTestContext(GrGLStandard forcedGpuAPI,
341                                            GLTestContext *shareContext) {
342     return new WasmWebGlTestContext();
343 }
344 } // namespace sk_gpu_test
345 
Init()346 void Init() { ToolUtils::UsePortableFontMgr(); }
347 
CurrentTestHarness()348 TestHarness CurrentTestHarness() {
349     return TestHarness::kWasmGMTests;
350 }
351 
EMSCRIPTEN_BINDINGS(GMs)352 EMSCRIPTEN_BINDINGS(GMs) {
353     function("Init", &Init);
354     function("ListGMs", &ListGMs);
355     function("ListTests", &ListTests);
356     function("LoadKnownDigest", &LoadKnownDigest);
357     function("_LoadResource", &LoadResource);
358     function("MakeGrContext", &MakeGrContext);
359     function("RunGM", &RunGM);
360     function("RunTest", &RunTest);
361 
362     class_<GrDirectContext>("GrDirectContext")
363         .smart_ptr<sk_sp<GrDirectContext>>("sk_sp<GrDirectContext>");
364 }
365