1/* 2* Copyright 2016 Google Inc. 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 "Viewer.h" 9 10#include "BisectSlide.h" 11#include "GMSlide.h" 12#include "ImageSlide.h" 13#include "Resources.h" 14#include "SampleSlide.h" 15#include "SkottieSlide.h" 16#include "SKPSlide.h" 17#include "SlideDir.h" 18 19#include "GrContext.h" 20#include "SkCanvas.h" 21#include "SkColorSpacePriv.h" 22#include "SkColorSpaceXformCanvas.h" 23#include "SkCommonFlags.h" 24#include "SkCommandLineFlags.h" 25#include "SkCommonFlagsGpu.h" 26#include "SkEventTracingPriv.h" 27#include "SkFontMgrPriv.h" 28#include "SkGraphics.h" 29#include "SkImagePriv.h" 30#include "SkOSFile.h" 31#include "SkOSPath.h" 32#include "SkPaintFilterCanvas.h" 33#include "SkPictureRecorder.h" 34#include "SkScan.h" 35#include "SkStream.h" 36#include "SkSurface.h" 37#include "SkTaskGroup.h" 38#include "SkTestFontMgr.h" 39#include "SkThreadedBMPDevice.h" 40 41#include "imgui.h" 42 43#include "ccpr/GrCoverageCountingPathRenderer.h" 44 45#include <stdlib.h> 46#include <map> 47 48using namespace sk_app; 49 50static std::map<GpuPathRenderers, std::string> gPathRendererNames; 51 52Application* Application::Create(int argc, char** argv, void* platformData) { 53 return new Viewer(argc, argv, platformData); 54} 55 56static DEFINE_string(slide, "", "Start on this sample."); 57static DEFINE_bool(list, false, "List samples?"); 58 59#ifdef SK_VULKAN 60# define BACKENDS_STR "\"sw\", \"gl\", and \"vk\"" 61#else 62# define BACKENDS_STR "\"sw\" and \"gl\"" 63#endif 64 65static DEFINE_string2(backend, b, "sw", "Backend to use. Allowed values are " BACKENDS_STR "."); 66 67static DEFINE_int32(msaa, 1, "Number of subpixel samples. 0 for no HW antialiasing."); 68 69DEFINE_string(bisect, "", "Path to a .skp or .svg file to bisect."); 70 71DECLARE_int32(threads) 72 73const char* kBackendTypeStrings[sk_app::Window::kBackendTypeCount] = { 74 "OpenGL", 75#if SK_ANGLE && defined(SK_BUILD_FOR_WIN) 76 "ANGLE", 77#endif 78#ifdef SK_VULKAN 79 "Vulkan", 80#endif 81 "Raster" 82}; 83 84static sk_app::Window::BackendType get_backend_type(const char* str) { 85#ifdef SK_VULKAN 86 if (0 == strcmp(str, "vk")) { 87 return sk_app::Window::kVulkan_BackendType; 88 } else 89#endif 90#if SK_ANGLE && defined(SK_BUILD_FOR_WIN) 91 if (0 == strcmp(str, "angle")) { 92 return sk_app::Window::kANGLE_BackendType; 93 } else 94#endif 95 if (0 == strcmp(str, "gl")) { 96 return sk_app::Window::kNativeGL_BackendType; 97 } else if (0 == strcmp(str, "sw")) { 98 return sk_app::Window::kRaster_BackendType; 99 } else { 100 SkDebugf("Unknown backend type, %s, defaulting to sw.", str); 101 return sk_app::Window::kRaster_BackendType; 102 } 103} 104 105static SkColorSpacePrimaries gSrgbPrimaries = { 106 0.64f, 0.33f, 107 0.30f, 0.60f, 108 0.15f, 0.06f, 109 0.3127f, 0.3290f }; 110 111static SkColorSpacePrimaries gAdobePrimaries = { 112 0.64f, 0.33f, 113 0.21f, 0.71f, 114 0.15f, 0.06f, 115 0.3127f, 0.3290f }; 116 117static SkColorSpacePrimaries gP3Primaries = { 118 0.680f, 0.320f, 119 0.265f, 0.690f, 120 0.150f, 0.060f, 121 0.3127f, 0.3290f }; 122 123static SkColorSpacePrimaries gRec2020Primaries = { 124 0.708f, 0.292f, 125 0.170f, 0.797f, 126 0.131f, 0.046f, 127 0.3127f, 0.3290f }; 128 129struct NamedPrimaries { 130 const char* fName; 131 SkColorSpacePrimaries* fPrimaries; 132} gNamedPrimaries[] = { 133 { "sRGB", &gSrgbPrimaries }, 134 { "AdobeRGB", &gAdobePrimaries }, 135 { "P3", &gP3Primaries }, 136 { "Rec. 2020", &gRec2020Primaries }, 137}; 138 139static bool primaries_equal(const SkColorSpacePrimaries& a, const SkColorSpacePrimaries& b) { 140 return memcmp(&a, &b, sizeof(SkColorSpacePrimaries)) == 0; 141} 142 143static Window::BackendType backend_type_for_window(Window::BackendType backendType) { 144 // In raster mode, we still use GL for the window. 145 // This lets us render the GUI faster (and correct). 146 return Window::kRaster_BackendType == backendType ? Window::kNativeGL_BackendType : backendType; 147} 148 149const char* kName = "name"; 150const char* kValue = "value"; 151const char* kOptions = "options"; 152const char* kSlideStateName = "Slide"; 153const char* kBackendStateName = "Backend"; 154const char* kMSAAStateName = "MSAA"; 155const char* kPathRendererStateName = "Path renderer"; 156const char* kSoftkeyStateName = "Softkey"; 157const char* kSoftkeyHint = "Please select a softkey"; 158const char* kFpsStateName = "FPS"; 159const char* kON = "ON"; 160const char* kOFF = "OFF"; 161const char* kRefreshStateName = "Refresh"; 162 163Viewer::Viewer(int argc, char** argv, void* platformData) 164 : fCurrentSlide(-1) 165 , fRefresh(false) 166 , fSaveToSKP(false) 167 , fShowImGuiDebugWindow(false) 168 , fShowSlidePicker(false) 169 , fShowImGuiTestWindow(false) 170 , fShowZoomWindow(false) 171 , fLastImage(nullptr) 172 , fBackendType(sk_app::Window::kNativeGL_BackendType) 173 , fColorMode(ColorMode::kLegacy) 174 , fColorSpacePrimaries(gSrgbPrimaries) 175 // Our UI can only tweak gamma (currently), so start out gamma-only 176 , fColorSpaceTransferFn(g2Dot2_TransferFn) 177 , fZoomLevel(0.0f) 178 , fGestureDevice(GestureDevice::kNone) 179 , fTileCnt(0) 180 , fThreadCnt(0) 181{ 182 SkGraphics::Init(); 183 184 gPathRendererNames[GpuPathRenderers::kAll] = "All Path Renderers"; 185 gPathRendererNames[GpuPathRenderers::kDefault] = 186 "Default Ganesh Behavior (best path renderer, not including CCPR)"; 187 gPathRendererNames[GpuPathRenderers::kStencilAndCover] = "NV_path_rendering"; 188 gPathRendererNames[GpuPathRenderers::kMSAA] = "Sample shading"; 189 gPathRendererNames[GpuPathRenderers::kSmall] = "Small paths (cached sdf or alpha masks)"; 190 gPathRendererNames[GpuPathRenderers::kCoverageCounting] = "Coverage counting"; 191 gPathRendererNames[GpuPathRenderers::kTessellating] = "Tessellating"; 192 gPathRendererNames[GpuPathRenderers::kNone] = "Software masks"; 193 194 SkDebugf("Command line arguments: "); 195 for (int i = 1; i < argc; ++i) { 196 SkDebugf("%s ", argv[i]); 197 } 198 SkDebugf("\n"); 199 200 SkCommandLineFlags::Parse(argc, argv); 201#ifdef SK_BUILD_FOR_ANDROID 202 SetResourcePath("/data/local/tmp/resources"); 203#endif 204 205 if (!FLAGS_nativeFonts) { 206 gSkFontMgr_DefaultFactory = &sk_tool_utils::MakePortableFontMgr; 207 } 208 209 initializeEventTracingForTools(); 210 static SkTaskGroup::Enabler kTaskGroupEnabler(FLAGS_threads); 211 212 fBackendType = get_backend_type(FLAGS_backend[0]); 213 fWindow = Window::CreateNativeWindow(platformData); 214 215 DisplayParams displayParams; 216 displayParams.fMSAASampleCount = FLAGS_msaa; 217 SetCtxOptionsFromCommonFlags(&displayParams.fGrContextOptions); 218 fWindow->setRequestedDisplayParams(displayParams); 219 220 // Configure timers 221 fStatsLayer.setActive(false); 222 fAnimateTimer = fStatsLayer.addTimer("Animate", SK_ColorMAGENTA, 0xffff66ff); 223 fPaintTimer = fStatsLayer.addTimer("Paint", SK_ColorGREEN); 224 fFlushTimer = fStatsLayer.addTimer("Flush", SK_ColorRED, 0xffff6666); 225 226 // register callbacks 227 fCommands.attach(fWindow); 228 fWindow->pushLayer(this); 229 fWindow->pushLayer(&fStatsLayer); 230 fWindow->pushLayer(&fImGuiLayer); 231 232 // add key-bindings 233 fCommands.addCommand(' ', "GUI", "Toggle Debug GUI", [this]() { 234 this->fShowImGuiDebugWindow = !this->fShowImGuiDebugWindow; 235 fWindow->inval(); 236 }); 237 // Command to jump directly to the slide picker and give it focus 238 fCommands.addCommand('/', "GUI", "Jump to slide picker", [this]() { 239 this->fShowImGuiDebugWindow = true; 240 this->fShowSlidePicker = true; 241 fWindow->inval(); 242 }); 243 // Alias that to Backspace, to match SampleApp 244 fCommands.addCommand(Window::Key::kBack, "Backspace", "GUI", "Jump to slide picker", [this]() { 245 this->fShowImGuiDebugWindow = true; 246 this->fShowSlidePicker = true; 247 fWindow->inval(); 248 }); 249 fCommands.addCommand('g', "GUI", "Toggle GUI Demo", [this]() { 250 this->fShowImGuiTestWindow = !this->fShowImGuiTestWindow; 251 fWindow->inval(); 252 }); 253 fCommands.addCommand('z', "GUI", "Toggle zoom window", [this]() { 254 this->fShowZoomWindow = !this->fShowZoomWindow; 255 fWindow->inval(); 256 }); 257 fCommands.addCommand('s', "Overlays", "Toggle stats display", [this]() { 258 fStatsLayer.setActive(!fStatsLayer.getActive()); 259 fWindow->inval(); 260 }); 261 fCommands.addCommand('0', "Overlays", "Reset stats", [this]() { 262 fStatsLayer.resetMeasurements(); 263 this->updateTitle(); 264 fWindow->inval(); 265 }); 266 fCommands.addCommand('c', "Modes", "Cycle color mode", [this]() { 267 switch (fColorMode) { 268 case ColorMode::kLegacy: 269 this->setColorMode(ColorMode::kColorManagedSRGB8888_NonLinearBlending); 270 break; 271 case ColorMode::kColorManagedSRGB8888_NonLinearBlending: 272 this->setColorMode(ColorMode::kColorManagedSRGB8888); 273 break; 274 case ColorMode::kColorManagedSRGB8888: 275 this->setColorMode(ColorMode::kColorManagedLinearF16); 276 break; 277 case ColorMode::kColorManagedLinearF16: 278 this->setColorMode(ColorMode::kLegacy); 279 break; 280 } 281 }); 282 fCommands.addCommand(Window::Key::kRight, "Right", "Navigation", "Next slide", [this]() { 283 this->setCurrentSlide(fCurrentSlide < fSlides.count() - 1 ? fCurrentSlide + 1 : 0); 284 }); 285 fCommands.addCommand(Window::Key::kLeft, "Left", "Navigation", "Previous slide", [this]() { 286 this->setCurrentSlide(fCurrentSlide > 0 ? fCurrentSlide - 1 : fSlides.count() - 1); 287 }); 288 fCommands.addCommand(Window::Key::kUp, "Up", "Transform", "Zoom in", [this]() { 289 this->changeZoomLevel(1.f / 32.f); 290 fWindow->inval(); 291 }); 292 fCommands.addCommand(Window::Key::kDown, "Down", "Transform", "Zoom out", [this]() { 293 this->changeZoomLevel(-1.f / 32.f); 294 fWindow->inval(); 295 }); 296 fCommands.addCommand('d', "Modes", "Change rendering backend", [this]() { 297 sk_app::Window::BackendType newBackend = (sk_app::Window::BackendType)( 298 (fBackendType + 1) % sk_app::Window::kBackendTypeCount); 299 // Switching to and from Vulkan is problematic on Linux so disabled for now 300#if defined(SK_BUILD_FOR_UNIX) && defined(SK_VULKAN) 301 if (newBackend == sk_app::Window::kVulkan_BackendType) { 302 newBackend = (sk_app::Window::BackendType)((newBackend + 1) % 303 sk_app::Window::kBackendTypeCount); 304 } else if (fBackendType == sk_app::Window::kVulkan_BackendType) { 305 newBackend = sk_app::Window::kVulkan_BackendType; 306 } 307#endif 308 this->setBackend(newBackend); 309 }); 310 fCommands.addCommand('+', "Threaded Backend", "Increase tile count", [this]() { 311 fTileCnt++; 312 if (fThreadCnt == 0) { 313 this->resetExecutor(); 314 } 315 this->updateTitle(); 316 fWindow->inval(); 317 }); 318 fCommands.addCommand('-', "Threaded Backend", "Decrease tile count", [this]() { 319 fTileCnt = SkTMax(0, fTileCnt - 1); 320 if (fThreadCnt == 0) { 321 this->resetExecutor(); 322 } 323 this->updateTitle(); 324 fWindow->inval(); 325 }); 326 fCommands.addCommand('>', "Threaded Backend", "Increase thread count", [this]() { 327 if (fTileCnt == 0) { 328 return; 329 } 330 fThreadCnt = (fThreadCnt + 1) % fTileCnt; 331 this->resetExecutor(); 332 this->updateTitle(); 333 fWindow->inval(); 334 }); 335 fCommands.addCommand('<', "Threaded Backend", "Decrease thread count", [this]() { 336 if (fTileCnt == 0) { 337 return; 338 } 339 fThreadCnt = (fThreadCnt + fTileCnt - 1) % fTileCnt; 340 this->resetExecutor(); 341 this->updateTitle(); 342 fWindow->inval(); 343 }); 344 fCommands.addCommand('K', "IO", "Save slide to SKP", [this]() { 345 fSaveToSKP = true; 346 fWindow->inval(); 347 }); 348 fCommands.addCommand('H', "Paint", "Hinting mode", [this]() { 349 if (!fPaintOverrides.fHinting) { 350 fPaintOverrides.fHinting = true; 351 fPaint.setHinting(SkPaint::kNo_Hinting); 352 } else { 353 switch (fPaint.getHinting()) { 354 case SkPaint::kNo_Hinting: 355 fPaint.setHinting(SkPaint::kSlight_Hinting); 356 break; 357 case SkPaint::kSlight_Hinting: 358 fPaint.setHinting(SkPaint::kNormal_Hinting); 359 break; 360 case SkPaint::kNormal_Hinting: 361 fPaint.setHinting(SkPaint::kFull_Hinting); 362 break; 363 case SkPaint::kFull_Hinting: 364 fPaint.setHinting(SkPaint::kNo_Hinting); 365 fPaintOverrides.fHinting = false; 366 break; 367 } 368 } 369 this->updateTitle(); 370 fWindow->inval(); 371 }); 372 fCommands.addCommand('A', "Paint", "Antialias Mode", [this]() { 373 if (!(fPaintOverrides.fFlags & SkPaint::kAntiAlias_Flag)) { 374 fPaintOverrides.fAntiAlias = SkPaintFields::AntiAliasState::Alias; 375 fPaintOverrides.fFlags |= SkPaint::kAntiAlias_Flag; 376 fPaint.setAntiAlias(false); 377 fPaintOverrides.fOriginalSkUseAnalyticAA = gSkUseAnalyticAA; 378 fPaintOverrides.fOriginalSkForceAnalyticAA = gSkForceAnalyticAA; 379 fPaintOverrides.fOriginalSkUseDeltaAA = gSkUseDeltaAA; 380 fPaintOverrides.fOriginalSkForceDeltaAA = gSkForceDeltaAA; 381 gSkUseAnalyticAA = gSkForceAnalyticAA = false; 382 gSkUseDeltaAA = gSkForceDeltaAA = false; 383 } else { 384 fPaint.setAntiAlias(true); 385 switch (fPaintOverrides.fAntiAlias) { 386 case SkPaintFields::AntiAliasState::Alias: 387 fPaintOverrides.fAntiAlias = SkPaintFields::AntiAliasState::Normal; 388 break; 389 case SkPaintFields::AntiAliasState::Normal: 390 fPaintOverrides.fAntiAlias = SkPaintFields::AntiAliasState::AnalyticAAEnabled; 391 gSkUseDeltaAA = gSkForceDeltaAA = false; 392 gSkUseAnalyticAA = true; 393 break; 394 case SkPaintFields::AntiAliasState::AnalyticAAEnabled: 395 fPaintOverrides.fAntiAlias = SkPaintFields::AntiAliasState::AnalyticAAForced; 396 gSkForceAnalyticAA = true; 397 break; 398 case SkPaintFields::AntiAliasState::AnalyticAAForced: 399 fPaintOverrides.fAntiAlias = SkPaintFields::AntiAliasState::DeltaAAEnabled; 400 gSkUseAnalyticAA = gSkForceAnalyticAA = false; 401 gSkUseDeltaAA = true; 402 break; 403 case SkPaintFields::AntiAliasState::DeltaAAEnabled: 404 fPaintOverrides.fAntiAlias = SkPaintFields::AntiAliasState::DeltaAAForced; 405 gSkForceDeltaAA = true; 406 break; 407 case SkPaintFields::AntiAliasState::DeltaAAForced: 408 fPaintOverrides.fAntiAlias = SkPaintFields::AntiAliasState::Alias; 409 fPaintOverrides.fFlags &= ~SkPaint::kAntiAlias_Flag; 410 gSkUseAnalyticAA = fPaintOverrides.fOriginalSkUseAnalyticAA; 411 gSkForceAnalyticAA = fPaintOverrides.fOriginalSkForceAnalyticAA; 412 gSkUseDeltaAA = fPaintOverrides.fOriginalSkUseDeltaAA; 413 gSkForceDeltaAA = fPaintOverrides.fOriginalSkForceDeltaAA; 414 break; 415 } 416 } 417 this->updateTitle(); 418 fWindow->inval(); 419 }); 420 fCommands.addCommand('L', "Paint", "Subpixel Antialias Mode", [this]() { 421 if (!(fPaintOverrides.fFlags & SkPaint::kLCDRenderText_Flag)) { 422 fPaintOverrides.fFlags |= SkPaint::kLCDRenderText_Flag; 423 fPaint.setLCDRenderText(false); 424 } else { 425 if (!fPaint.isLCDRenderText()) { 426 fPaint.setLCDRenderText(true); 427 } else { 428 fPaintOverrides.fFlags &= ~SkPaint::kLCDRenderText_Flag; 429 } 430 } 431 this->updateTitle(); 432 fWindow->inval(); 433 }); 434 fCommands.addCommand('S', "Paint", "Subpixel Position Mode", [this]() { 435 if (!(fPaintOverrides.fFlags & SkPaint::kSubpixelText_Flag)) { 436 fPaintOverrides.fFlags |= SkPaint::kSubpixelText_Flag; 437 fPaint.setSubpixelText(false); 438 } else { 439 if (!fPaint.isSubpixelText()) { 440 fPaint.setSubpixelText(true); 441 } else { 442 fPaintOverrides.fFlags &= ~SkPaint::kSubpixelText_Flag; 443 } 444 } 445 this->updateTitle(); 446 fWindow->inval(); 447 }); 448 449 // set up slides 450 this->initSlides(); 451 if (FLAGS_list) { 452 this->listNames(); 453 } 454 455 fAnimTimer.run(); 456 457 auto gamutImage = GetResourceAsImage("images/gamut.png"); 458 if (gamutImage) { 459 fImGuiGamutPaint.setShader(gamutImage->makeShader()); 460 } 461 fImGuiGamutPaint.setColor(SK_ColorWHITE); 462 fImGuiGamutPaint.setFilterQuality(kLow_SkFilterQuality); 463 464 fWindow->attach(backend_type_for_window(fBackendType)); 465 this->setCurrentSlide(this->startupSlide()); 466} 467 468void Viewer::initSlides() { 469 fAllSlideNames = Json::Value(Json::arrayValue); 470 471 // Bisect slide. 472 if (!FLAGS_bisect.isEmpty()) { 473 sk_sp<BisectSlide> bisect = BisectSlide::Create(FLAGS_bisect[0]); 474 if (bisect && !SkCommandLineFlags::ShouldSkip(FLAGS_match, bisect->getName().c_str())) { 475 if (FLAGS_bisect.count() >= 2) { 476 for (const char* ch = FLAGS_bisect[1]; *ch; ++ch) { 477 bisect->onChar(*ch); 478 } 479 } 480 fSlides.push_back(std::move(bisect)); 481 } 482 } 483 484 // GMs 485 int firstGM = fSlides.count(); 486 const skiagm::GMRegistry* gms(skiagm::GMRegistry::Head()); 487 while (gms) { 488 std::unique_ptr<skiagm::GM> gm(gms->factory()(nullptr)); 489 490 if (!SkCommandLineFlags::ShouldSkip(FLAGS_match, gm->getName())) { 491 sk_sp<Slide> slide(new GMSlide(gm.release())); 492 fSlides.push_back(std::move(slide)); 493 } 494 495 gms = gms->next(); 496 } 497 // reverse gms 498 int numGMs = fSlides.count() - firstGM; 499 for (int i = 0; i < numGMs/2; ++i) { 500 std::swap(fSlides[firstGM + i], fSlides[fSlides.count() - i - 1]); 501 } 502 503 // samples 504 const SkViewRegister* reg = SkViewRegister::Head(); 505 while (reg) { 506 sk_sp<Slide> slide(new SampleSlide(reg->factory())); 507 if (!SkCommandLineFlags::ShouldSkip(FLAGS_match, slide->getName().c_str())) { 508 fSlides.push_back(slide); 509 } 510 reg = reg->next(); 511 } 512 513 // SKPs 514 for (int i = 0; i < FLAGS_skps.count(); i++) { 515 if (SkStrEndsWith(FLAGS_skps[i], ".skp")) { 516 if (SkCommandLineFlags::ShouldSkip(FLAGS_match, FLAGS_skps[i])) { 517 continue; 518 } 519 520 SkString path(FLAGS_skps[i]); 521 sk_sp<SKPSlide> slide(new SKPSlide(SkOSPath::Basename(path.c_str()), path)); 522 if (slide) { 523 fSlides.push_back(slide); 524 } 525 } else { 526 SkOSFile::Iter it(FLAGS_skps[i], ".skp"); 527 SkString skpName; 528 while (it.next(&skpName)) { 529 if (SkCommandLineFlags::ShouldSkip(FLAGS_match, skpName.c_str())) { 530 continue; 531 } 532 533 SkString path = SkOSPath::Join(FLAGS_skps[i], skpName.c_str()); 534 sk_sp<SKPSlide> slide(new SKPSlide(skpName, path)); 535 if (slide) { 536 fSlides.push_back(slide); 537 } 538 } 539 } 540 } 541 542 // JPGs 543 for (int i = 0; i < FLAGS_jpgs.count(); i++) { 544 SkOSFile::Iter it(FLAGS_jpgs[i], ".jpg"); 545 SkString jpgName; 546 while (it.next(&jpgName)) { 547 if (SkCommandLineFlags::ShouldSkip(FLAGS_match, jpgName.c_str())) { 548 continue; 549 } 550 551 SkString path = SkOSPath::Join(FLAGS_jpgs[i], jpgName.c_str()); 552 sk_sp<ImageSlide> slide(new ImageSlide(jpgName, path)); 553 if (slide) { 554 fSlides.push_back(slide); 555 } 556 } 557 } 558 559 // JSONs 560 for (const auto& json : FLAGS_jsons) { 561 SkTArray<sk_sp<Slide>, true> dirSlides; 562 563 SkOSFile::Iter it(json.c_str(), ".json"); 564 SkString jsonName; 565 while (it.next(&jsonName)) { 566 if (SkCommandLineFlags::ShouldSkip(FLAGS_match, jsonName.c_str())) { 567 continue; 568 } 569 auto slide = sk_make_sp<SkottieSlide>(jsonName, SkOSPath::Join(json.c_str(), 570 jsonName.c_str())); 571 dirSlides.push_back(slide); 572 fSlides.push_back(std::move(slide)); 573 } 574 575 if (!dirSlides.empty()) { 576 fSlides.push_back(sk_make_sp<SlideDir>(SkStringPrintf("skottie-dir[%s]", json.c_str()), 577 std::move(dirSlides))); 578 } 579 } 580} 581 582 583Viewer::~Viewer() { 584 fWindow->detach(); 585 delete fWindow; 586} 587 588struct SkPaintTitleUpdater { 589 SkPaintTitleUpdater(SkString* title) : fTitle(title), fCount(0) {} 590 void append(const char* s) { 591 if (fCount == 0) { 592 fTitle->append(" {"); 593 } else { 594 fTitle->append(", "); 595 } 596 fTitle->append(s); 597 ++fCount; 598 } 599 void done() { 600 if (fCount > 0) { 601 fTitle->append("}"); 602 } 603 } 604 SkString* fTitle; 605 int fCount; 606}; 607 608void Viewer::updateTitle() { 609 if (!fWindow) { 610 return; 611 } 612 if (fWindow->sampleCount() < 1) { 613 return; // Surface hasn't been created yet. 614 } 615 616 SkString title("Viewer: "); 617 title.append(fSlides[fCurrentSlide]->getName()); 618 619 if (gSkUseDeltaAA) { 620 if (gSkForceDeltaAA) { 621 title.append(" <FDAA>"); 622 } else { 623 title.append(" <DAA>"); 624 } 625 } else if (gSkUseAnalyticAA) { 626 if (gSkForceAnalyticAA) { 627 title.append(" <FAAA>"); 628 } else { 629 title.append(" <AAA>"); 630 } 631 } 632 633 SkPaintTitleUpdater paintTitle(&title); 634 if (fPaintOverrides.fFlags & SkPaint::kAntiAlias_Flag) { 635 if (fPaint.isAntiAlias()) { 636 paintTitle.append("Antialias"); 637 } else { 638 paintTitle.append("Alias"); 639 } 640 } 641 if (fPaintOverrides.fFlags & SkPaint::kLCDRenderText_Flag) { 642 if (fPaint.isLCDRenderText()) { 643 paintTitle.append("LCD"); 644 } else { 645 paintTitle.append("lcd"); 646 } 647 } 648 if (fPaintOverrides.fFlags & SkPaint::kSubpixelText_Flag) { 649 if (fPaint.isSubpixelText()) { 650 paintTitle.append("Subpixel Glyphs"); 651 } else { 652 paintTitle.append("Pixel Glyphs"); 653 } 654 } 655 if (fPaintOverrides.fHinting) { 656 switch (fPaint.getHinting()) { 657 case SkPaint::kNo_Hinting: 658 paintTitle.append("No Hinting"); 659 break; 660 case SkPaint::kSlight_Hinting: 661 paintTitle.append("Slight Hinting"); 662 break; 663 case SkPaint::kNormal_Hinting: 664 paintTitle.append("Normal Hinting"); 665 break; 666 case SkPaint::kFull_Hinting: 667 paintTitle.append("Full Hinting"); 668 break; 669 } 670 } 671 paintTitle.done(); 672 673 if (fTileCnt > 0) { 674 title.appendf(" T%d", fTileCnt); 675 if (fThreadCnt > 0) { 676 title.appendf("/%d", fThreadCnt); 677 } 678 } 679 680 switch (fColorMode) { 681 case ColorMode::kLegacy: 682 title.append(" Legacy 8888"); 683 break; 684 case ColorMode::kColorManagedSRGB8888_NonLinearBlending: 685 title.append(" ColorManaged 8888 (Nonlinear blending)"); 686 break; 687 case ColorMode::kColorManagedSRGB8888: 688 title.append(" ColorManaged 8888"); 689 break; 690 case ColorMode::kColorManagedLinearF16: 691 title.append(" ColorManaged F16"); 692 break; 693 } 694 695 if (ColorMode::kLegacy != fColorMode) { 696 int curPrimaries = -1; 697 for (size_t i = 0; i < SK_ARRAY_COUNT(gNamedPrimaries); ++i) { 698 if (primaries_equal(*gNamedPrimaries[i].fPrimaries, fColorSpacePrimaries)) { 699 curPrimaries = i; 700 break; 701 } 702 } 703 title.appendf(" %s", curPrimaries >= 0 ? gNamedPrimaries[curPrimaries].fName : "Custom"); 704 705 if (ColorMode::kColorManagedSRGB8888_NonLinearBlending == fColorMode) { 706 title.appendf(" Gamma %f", fColorSpaceTransferFn.fG); 707 } 708 } 709 710 title.append(" ["); 711 title.append(kBackendTypeStrings[fBackendType]); 712 int msaa = fWindow->sampleCount(); 713 if (msaa > 1) { 714 title.appendf(" MSAA: %i", msaa); 715 } 716 title.append("]"); 717 718 GpuPathRenderers pr = fWindow->getRequestedDisplayParams().fGrContextOptions.fGpuPathRenderers; 719 if (GpuPathRenderers::kDefault != pr) { 720 title.appendf(" [Path renderer: %s]", gPathRendererNames[pr].c_str()); 721 } 722 723 fWindow->setTitle(title.c_str()); 724} 725 726int Viewer::startupSlide() const { 727 728 if (!FLAGS_slide.isEmpty()) { 729 int count = fSlides.count(); 730 for (int i = 0; i < count; i++) { 731 if (fSlides[i]->getName().equals(FLAGS_slide[0])) { 732 return i; 733 } 734 } 735 736 fprintf(stderr, "Unknown slide \"%s\"\n", FLAGS_slide[0]); 737 this->listNames(); 738 } 739 740 return 0; 741} 742 743void Viewer::listNames() const { 744 SkDebugf("All Slides:\n"); 745 for (const auto& slide : fSlides) { 746 SkDebugf(" %s\n", slide->getName().c_str()); 747 } 748} 749 750void Viewer::setCurrentSlide(int slide) { 751 SkASSERT(slide >= 0 && slide < fSlides.count()); 752 753 if (slide == fCurrentSlide) { 754 return; 755 } 756 757 if (fCurrentSlide >= 0) { 758 fSlides[fCurrentSlide]->unload(); 759 } 760 761 fSlides[slide]->load(SkIntToScalar(fWindow->width()), 762 SkIntToScalar(fWindow->height())); 763 fCurrentSlide = slide; 764 this->setupCurrentSlide(); 765} 766 767void Viewer::setupCurrentSlide() { 768 if (fCurrentSlide >= 0) { 769 // prepare dimensions for image slides 770 fGesture.resetTouchState(); 771 fDefaultMatrix.reset(); 772 773 const SkISize slideSize = fSlides[fCurrentSlide]->getDimensions(); 774 const SkRect slideBounds = SkRect::MakeIWH(slideSize.width(), slideSize.height()); 775 const SkRect windowRect = SkRect::MakeIWH(fWindow->width(), fWindow->height()); 776 777 // Start with a matrix that scales the slide to the available screen space 778 if (fWindow->scaleContentToFit()) { 779 if (windowRect.width() > 0 && windowRect.height() > 0) { 780 fDefaultMatrix.setRectToRect(slideBounds, windowRect, SkMatrix::kStart_ScaleToFit); 781 } 782 } 783 784 // Prevent the user from dragging content so far outside the window they can't find it again 785 fGesture.setTransLimit(slideBounds, windowRect, fDefaultMatrix); 786 787 this->updateTitle(); 788 this->updateUIState(); 789 790 fStatsLayer.resetMeasurements(); 791 792 fWindow->inval(); 793 } 794} 795 796#define MAX_ZOOM_LEVEL 8 797#define MIN_ZOOM_LEVEL -8 798 799void Viewer::changeZoomLevel(float delta) { 800 fZoomLevel += delta; 801 fZoomLevel = SkScalarPin(fZoomLevel, MIN_ZOOM_LEVEL, MAX_ZOOM_LEVEL); 802} 803 804SkMatrix Viewer::computeMatrix() { 805 SkMatrix m; 806 807 SkScalar zoomScale = (fZoomLevel < 0) ? SK_Scalar1 / (SK_Scalar1 - fZoomLevel) 808 : SK_Scalar1 + fZoomLevel; 809 m = fGesture.localM(); 810 m.preConcat(fGesture.globalM()); 811 m.preConcat(fDefaultMatrix); 812 m.preScale(zoomScale, zoomScale); 813 814 return m; 815} 816 817void Viewer::setBackend(sk_app::Window::BackendType backendType) { 818 fBackendType = backendType; 819 820 fWindow->detach(); 821 822#if defined(SK_BUILD_FOR_WIN) 823 // Switching between OpenGL, Vulkan, and ANGLE in the same window is problematic at this point 824 // on Windows, so we just delete the window and recreate it. 825 DisplayParams params = fWindow->getRequestedDisplayParams(); 826 delete fWindow; 827 fWindow = Window::CreateNativeWindow(nullptr); 828 829 // re-register callbacks 830 fCommands.attach(fWindow); 831 fWindow->pushLayer(this); 832 fWindow->pushLayer(&fStatsLayer); 833 fWindow->pushLayer(&fImGuiLayer); 834 835 // Don't allow the window to re-attach. If we're in MSAA mode, the params we grabbed above 836 // will still include our correct sample count. But the re-created fWindow will lose that 837 // information. On Windows, we need to re-create the window when changing sample count, 838 // so we'll incorrectly detect that situation, then re-initialize the window in GL mode, 839 // rendering this tear-down step pointless (and causing the Vulkan window context to fail 840 // as if we had never changed windows at all). 841 fWindow->setRequestedDisplayParams(params, false); 842#endif 843 844 fWindow->attach(backend_type_for_window(fBackendType)); 845} 846 847void Viewer::setColorMode(ColorMode colorMode) { 848 fColorMode = colorMode; 849 850 // When we're in color managed mode, we tag our window surface as sRGB. If we've switched into 851 // or out of legacy/nonlinear mode, we need to update our window configuration. 852 DisplayParams params = fWindow->getRequestedDisplayParams(); 853 bool wasInLegacy = !SkToBool(params.fColorSpace); 854 bool wantLegacy = (ColorMode::kLegacy == fColorMode) || 855 (ColorMode::kColorManagedSRGB8888_NonLinearBlending == fColorMode); 856 if (wasInLegacy != wantLegacy) { 857 params.fColorSpace = wantLegacy ? nullptr : SkColorSpace::MakeSRGB(); 858 fWindow->setRequestedDisplayParams(params); 859 } 860 861 this->updateTitle(); 862 fWindow->inval(); 863} 864 865class OveridePaintFilterCanvas : public SkPaintFilterCanvas { 866public: 867 OveridePaintFilterCanvas(SkCanvas* canvas, SkPaint* paint, Viewer::SkPaintFields* fields) 868 : SkPaintFilterCanvas(canvas), fPaint(paint), fPaintOverrides(fields) 869 { } 870 bool onFilter(SkTCopyOnFirstWrite<SkPaint>* paint, Type) const override { 871 if (fPaintOverrides->fHinting) { 872 paint->writable()->setHinting(fPaint->getHinting()); 873 } 874 if (fPaintOverrides->fFlags & SkPaint::kAntiAlias_Flag) { 875 paint->writable()->setAntiAlias(fPaint->isAntiAlias()); 876 } 877 if (fPaintOverrides->fFlags & SkPaint::kLCDRenderText_Flag) { 878 paint->writable()->setLCDRenderText(fPaint->isLCDRenderText()); 879 } 880 if (fPaintOverrides->fFlags & SkPaint::kSubpixelText_Flag) { 881 paint->writable()->setSubpixelText(fPaint->isSubpixelText()); 882 } 883 return true; 884 } 885 SkPaint* fPaint; 886 Viewer::SkPaintFields* fPaintOverrides; 887}; 888 889void Viewer::drawSlide(SkCanvas* canvas) { 890 SkAutoCanvasRestore autorestore(canvas, false); 891 892 // By default, we render directly into the window's surface/canvas 893 SkCanvas* slideCanvas = canvas; 894 fLastImage.reset(); 895 896 // If we're in any of the color managed modes, construct the color space we're going to use 897 sk_sp<SkColorSpace> cs = nullptr; 898 if (ColorMode::kLegacy != fColorMode) { 899 auto transferFn = (ColorMode::kColorManagedLinearF16 == fColorMode) 900 ? SkColorSpace::kLinear_RenderTargetGamma : SkColorSpace::kSRGB_RenderTargetGamma; 901 SkMatrix44 toXYZ(SkMatrix44::kIdentity_Constructor); 902 SkAssertResult(fColorSpacePrimaries.toXYZD50(&toXYZ)); 903 if (ColorMode::kColorManagedSRGB8888_NonLinearBlending == fColorMode) { 904 cs = SkColorSpace::MakeRGB(fColorSpaceTransferFn, toXYZ); 905 } else { 906 cs = SkColorSpace::MakeRGB(transferFn, toXYZ); 907 } 908 } 909 910 if (fSaveToSKP) { 911 SkPictureRecorder recorder; 912 SkCanvas* recorderCanvas = recorder.beginRecording( 913 SkRect::Make(fSlides[fCurrentSlide]->getDimensions())); 914 // In xform-canvas mode, record the transformed output 915 std::unique_ptr<SkCanvas> xformCanvas = nullptr; 916 if (ColorMode::kColorManagedSRGB8888_NonLinearBlending == fColorMode) { 917 xformCanvas = SkCreateColorSpaceXformCanvas(recorderCanvas, cs); 918 recorderCanvas = xformCanvas.get(); 919 } 920 fSlides[fCurrentSlide]->draw(recorderCanvas); 921 sk_sp<SkPicture> picture(recorder.finishRecordingAsPicture()); 922 SkFILEWStream stream("sample_app.skp"); 923 picture->serialize(&stream); 924 fSaveToSKP = false; 925 } 926 927 // If we're in F16, or we're zooming, or we're in color correct 8888 and the gamut isn't sRGB, 928 // we need to render offscreen. We also need to render offscreen if we're in any raster mode, 929 // because the window surface is actually GL. 930 sk_sp<SkSurface> offscreenSurface = nullptr; 931 std::unique_ptr<SkThreadedBMPDevice> threadedDevice; 932 std::unique_ptr<SkCanvas> threadedCanvas; 933 if (Window::kRaster_BackendType == fBackendType || 934 ColorMode::kColorManagedLinearF16 == fColorMode || 935 fShowZoomWindow || 936 (ColorMode::kColorManagedSRGB8888 == fColorMode && 937 !primaries_equal(fColorSpacePrimaries, gSrgbPrimaries))) { 938 939 SkColorType colorType = (ColorMode::kColorManagedLinearF16 == fColorMode) 940 ? kRGBA_F16_SkColorType : kN32_SkColorType; 941 // In nonlinear blending mode, we actually use a legacy off-screen canvas, and wrap it 942 // with a special canvas (below) that has the color space attached 943 sk_sp<SkColorSpace> offscreenColorSpace = 944 (ColorMode::kColorManagedSRGB8888_NonLinearBlending == fColorMode) ? nullptr : cs; 945 SkImageInfo info = SkImageInfo::Make(fWindow->width(), fWindow->height(), colorType, 946 kPremul_SkAlphaType, std::move(offscreenColorSpace)); 947 offscreenSurface = Window::kRaster_BackendType == fBackendType ? SkSurface::MakeRaster(info) 948 : canvas->makeSurface(info); 949 SkPixmap offscreenPixmap; 950 if (fTileCnt > 0 && offscreenSurface->peekPixels(&offscreenPixmap)) { 951 SkBitmap offscreenBitmap; 952 offscreenBitmap.installPixels(offscreenPixmap); 953 threadedDevice.reset(new SkThreadedBMPDevice(offscreenBitmap, fTileCnt, 954 fThreadCnt, fExecutor.get())); 955 threadedCanvas.reset(new SkCanvas(threadedDevice.get())); 956 slideCanvas = threadedCanvas.get(); 957 } else { 958 slideCanvas = offscreenSurface->getCanvas(); 959 } 960 } 961 962 std::unique_ptr<SkCanvas> xformCanvas = nullptr; 963 if (ColorMode::kColorManagedSRGB8888_NonLinearBlending == fColorMode) { 964 xformCanvas = SkCreateColorSpaceXformCanvas(slideCanvas, cs); 965 slideCanvas = xformCanvas.get(); 966 } 967 968 int count = slideCanvas->save(); 969 slideCanvas->clear(SK_ColorWHITE); 970 slideCanvas->concat(computeMatrix()); 971 // Time the painting logic of the slide 972 fStatsLayer.beginTiming(fPaintTimer); 973 OveridePaintFilterCanvas filterCanvas(slideCanvas, &fPaint, &fPaintOverrides); 974 fSlides[fCurrentSlide]->draw(&filterCanvas); 975 fStatsLayer.endTiming(fPaintTimer); 976 slideCanvas->restoreToCount(count); 977 978 // Force a flush so we can time that, too 979 fStatsLayer.beginTiming(fFlushTimer); 980 slideCanvas->flush(); 981 fStatsLayer.endTiming(fFlushTimer); 982 983 // If we rendered offscreen, snap an image and push the results to the window's canvas 984 if (offscreenSurface) { 985 fLastImage = offscreenSurface->makeImageSnapshot(); 986 987 // Tag the image with the sRGB gamut, so no further color space conversion happens 988 sk_sp<SkColorSpace> srgb = (ColorMode::kColorManagedLinearF16 == fColorMode) 989 ? SkColorSpace::MakeSRGBLinear() : SkColorSpace::MakeSRGB(); 990 auto retaggedImage = SkImageMakeRasterCopyAndAssignColorSpace(fLastImage.get(), srgb.get()); 991 SkPaint paint; 992 paint.setBlendMode(SkBlendMode::kSrc); 993 canvas->drawImage(retaggedImage, 0, 0, &paint); 994 } 995} 996 997void Viewer::onBackendCreated() { 998 this->setupCurrentSlide(); 999 fWindow->show(); 1000} 1001 1002void Viewer::onPaint(SkCanvas* canvas) { 1003 this->drawSlide(canvas); 1004 1005 fCommands.drawHelp(canvas); 1006 1007 this->drawImGui(); 1008 1009 // Update the FPS 1010 this->updateUIState(); 1011} 1012 1013SkPoint Viewer::mapEvent(float x, float y) { 1014 const auto m = this->computeMatrix(); 1015 SkMatrix inv; 1016 1017 SkAssertResult(m.invert(&inv)); 1018 1019 return inv.mapXY(x, y); 1020} 1021 1022bool Viewer::onTouch(intptr_t owner, Window::InputState state, float x, float y) { 1023 if (GestureDevice::kMouse == fGestureDevice) { 1024 return false; 1025 } 1026 1027 const auto slidePt = this->mapEvent(x, y); 1028 if (fSlides[fCurrentSlide]->onMouse(slidePt.x(), slidePt.y(), state, 0)) { 1029 fWindow->inval(); 1030 return true; 1031 } 1032 1033 void* castedOwner = reinterpret_cast<void*>(owner); 1034 switch (state) { 1035 case Window::kUp_InputState: { 1036 fGesture.touchEnd(castedOwner); 1037 break; 1038 } 1039 case Window::kDown_InputState: { 1040 fGesture.touchBegin(castedOwner, x, y); 1041 break; 1042 } 1043 case Window::kMove_InputState: { 1044 fGesture.touchMoved(castedOwner, x, y); 1045 break; 1046 } 1047 } 1048 fGestureDevice = fGesture.isBeingTouched() ? GestureDevice::kTouch : GestureDevice::kNone; 1049 fWindow->inval(); 1050 return true; 1051} 1052 1053bool Viewer::onMouse(int x, int y, Window::InputState state, uint32_t modifiers) { 1054 if (GestureDevice::kTouch == fGestureDevice) { 1055 return false; 1056 } 1057 1058 const auto slidePt = this->mapEvent(x, y); 1059 if (fSlides[fCurrentSlide]->onMouse(slidePt.x(), slidePt.y(), state, modifiers)) { 1060 fWindow->inval(); 1061 return true; 1062 } 1063 1064 switch (state) { 1065 case Window::kUp_InputState: { 1066 fGesture.touchEnd(nullptr); 1067 break; 1068 } 1069 case Window::kDown_InputState: { 1070 fGesture.touchBegin(nullptr, x, y); 1071 break; 1072 } 1073 case Window::kMove_InputState: { 1074 fGesture.touchMoved(nullptr, x, y); 1075 break; 1076 } 1077 } 1078 fGestureDevice = fGesture.isBeingTouched() ? GestureDevice::kMouse : GestureDevice::kNone; 1079 1080 if (state != Window::kMove_InputState || fGesture.isBeingTouched()) { 1081 fWindow->inval(); 1082 } 1083 return true; 1084} 1085 1086static ImVec2 ImGui_DragPrimary(const char* label, float* x, float* y, 1087 const ImVec2& pos, const ImVec2& size) { 1088 // Transform primaries ([0, 0] - [0.8, 0.9]) to screen coords (including Y-flip) 1089 ImVec2 center(pos.x + (*x / 0.8f) * size.x, pos.y + (1.0f - (*y / 0.9f)) * size.y); 1090 1091 // Invisible 10x10 button 1092 ImGui::SetCursorScreenPos(ImVec2(center.x - 5, center.y - 5)); 1093 ImGui::InvisibleButton(label, ImVec2(10, 10)); 1094 1095 if (ImGui::IsItemActive() && ImGui::IsMouseDragging()) { 1096 ImGuiIO& io = ImGui::GetIO(); 1097 // Normalized mouse position, relative to our gamut box 1098 ImVec2 mousePosXY((io.MousePos.x - pos.x) / size.x, (io.MousePos.y - pos.y) / size.y); 1099 // Clamp to edge of box, convert back to primary scale 1100 *x = SkTPin(mousePosXY.x, 0.0f, 1.0f) * 0.8f; 1101 *y = SkTPin(1 - mousePosXY.y, 0.0f, 1.0f) * 0.9f; 1102 } 1103 1104 if (ImGui::IsItemHovered()) { 1105 ImGui::SetTooltip("x: %.3f\ny: %.3f", *x, *y); 1106 } 1107 1108 // Return screen coordinates for the caller. We could just return center here, but we'd have 1109 // one frame of lag during drag. 1110 return ImVec2(pos.x + (*x / 0.8f) * size.x, pos.y + (1.0f - (*y / 0.9f)) * size.y); 1111} 1112 1113static void ImGui_Primaries(SkColorSpacePrimaries* primaries, SkPaint* gamutPaint) { 1114 ImDrawList* drawList = ImGui::GetWindowDrawList(); 1115 1116 // The gamut image covers a (0.8 x 0.9) shaped region, so fit our image/canvas to the available 1117 // width, and scale the height to maintain aspect ratio. 1118 float canvasWidth = SkTMax(ImGui::GetContentRegionAvailWidth(), 50.0f); 1119 ImVec2 size = ImVec2(canvasWidth, canvasWidth * (0.9f / 0.8f)); 1120 ImVec2 pos = ImGui::GetCursorScreenPos(); 1121 1122 // Background image. Only draw a subset of the image, to avoid the regions less than zero. 1123 // Simplifes re-mapping math, clipping behavior, and increases resolution in the useful area. 1124 // Magic numbers are pixel locations of the origin and upper-right corner. 1125 drawList->AddImage(gamutPaint, pos, ImVec2(pos.x + size.x, pos.y + size.y), 1126 ImVec2(242, 61), ImVec2(1897, 1922)); 1127 ImVec2 endPos = ImGui::GetCursorPos(); 1128 1129 // Primary markers 1130 ImVec2 r = ImGui_DragPrimary("R", &primaries->fRX, &primaries->fRY, pos, size); 1131 ImVec2 g = ImGui_DragPrimary("G", &primaries->fGX, &primaries->fGY, pos, size); 1132 ImVec2 b = ImGui_DragPrimary("B", &primaries->fBX, &primaries->fBY, pos, size); 1133 ImVec2 w = ImGui_DragPrimary("W", &primaries->fWX, &primaries->fWY, pos, size); 1134 1135 // Gamut triangle 1136 drawList->AddCircle(r, 5.0f, 0xFF000040); 1137 drawList->AddCircle(g, 5.0f, 0xFF004000); 1138 drawList->AddCircle(b, 5.0f, 0xFF400000); 1139 drawList->AddCircle(w, 5.0f, 0xFFFFFFFF); 1140 drawList->AddTriangle(r, g, b, 0xFFFFFFFF); 1141 1142 // Re-position cursor immediate after the diagram for subsequent controls 1143 ImGui::SetCursorPos(endPos); 1144} 1145 1146void Viewer::drawImGui() { 1147 // Support drawing the ImGui demo window. Superfluous, but gives a good idea of what's possible 1148 if (fShowImGuiTestWindow) { 1149 ImGui::ShowTestWindow(&fShowImGuiTestWindow); 1150 } 1151 1152 if (fShowImGuiDebugWindow) { 1153 // We have some dynamic content that sizes to fill available size. If the scroll bar isn't 1154 // always visible, we can end up in a layout feedback loop. 1155 ImGui::SetNextWindowSize(ImVec2(400, 400), ImGuiSetCond_FirstUseEver); 1156 DisplayParams params = fWindow->getRequestedDisplayParams(); 1157 bool paramsChanged = false; 1158 if (ImGui::Begin("Tools", &fShowImGuiDebugWindow, 1159 ImGuiWindowFlags_AlwaysVerticalScrollbar)) { 1160 if (ImGui::CollapsingHeader("Backend")) { 1161 int newBackend = static_cast<int>(fBackendType); 1162 ImGui::RadioButton("Raster", &newBackend, sk_app::Window::kRaster_BackendType); 1163 ImGui::SameLine(); 1164 ImGui::RadioButton("OpenGL", &newBackend, sk_app::Window::kNativeGL_BackendType); 1165#if SK_ANGLE && defined(SK_BUILD_FOR_WIN) 1166 ImGui::SameLine(); 1167 ImGui::RadioButton("ANGLE", &newBackend, sk_app::Window::kANGLE_BackendType); 1168#endif 1169#if defined(SK_VULKAN) 1170 ImGui::SameLine(); 1171 ImGui::RadioButton("Vulkan", &newBackend, sk_app::Window::kVulkan_BackendType); 1172#endif 1173 if (newBackend != fBackendType) { 1174 fDeferredActions.push_back([=]() { 1175 this->setBackend(static_cast<sk_app::Window::BackendType>(newBackend)); 1176 }); 1177 } 1178 1179 const GrContext* ctx = fWindow->getGrContext(); 1180 bool* wire = ¶ms.fGrContextOptions.fWireframeMode; 1181 if (ctx && ImGui::Checkbox("Wireframe Mode", wire)) { 1182 paramsChanged = true; 1183 } 1184 1185 if (ctx) { 1186 int sampleCount = fWindow->sampleCount(); 1187 ImGui::Text("MSAA: "); ImGui::SameLine(); 1188 ImGui::RadioButton("1", &sampleCount, 1); ImGui::SameLine(); 1189 ImGui::RadioButton("4", &sampleCount, 4); ImGui::SameLine(); 1190 ImGui::RadioButton("8", &sampleCount, 8); ImGui::SameLine(); 1191 ImGui::RadioButton("16", &sampleCount, 16); 1192 1193 if (sampleCount != params.fMSAASampleCount) { 1194 params.fMSAASampleCount = sampleCount; 1195 paramsChanged = true; 1196 } 1197 } 1198 1199 if (ImGui::TreeNode("Path Renderers")) { 1200 GpuPathRenderers prevPr = params.fGrContextOptions.fGpuPathRenderers; 1201 auto prButton = [&](GpuPathRenderers x) { 1202 if (ImGui::RadioButton(gPathRendererNames[x].c_str(), prevPr == x)) { 1203 if (x != params.fGrContextOptions.fGpuPathRenderers) { 1204 params.fGrContextOptions.fGpuPathRenderers = x; 1205 paramsChanged = true; 1206 } 1207 } 1208 }; 1209 1210 if (!ctx) { 1211 ImGui::RadioButton("Software", true); 1212 } else if (fWindow->sampleCount() > 1) { 1213 prButton(GpuPathRenderers::kDefault); 1214 prButton(GpuPathRenderers::kAll); 1215 if (ctx->caps()->shaderCaps()->pathRenderingSupport()) { 1216 prButton(GpuPathRenderers::kStencilAndCover); 1217 } 1218 if (ctx->caps()->sampleShadingSupport()) { 1219 prButton(GpuPathRenderers::kMSAA); 1220 } 1221 prButton(GpuPathRenderers::kTessellating); 1222 prButton(GpuPathRenderers::kNone); 1223 } else { 1224 prButton(GpuPathRenderers::kDefault); 1225 prButton(GpuPathRenderers::kAll); 1226 if (GrCoverageCountingPathRenderer::IsSupported(*ctx->caps())) { 1227 prButton(GpuPathRenderers::kCoverageCounting); 1228 } 1229 prButton(GpuPathRenderers::kSmall); 1230 prButton(GpuPathRenderers::kTessellating); 1231 prButton(GpuPathRenderers::kNone); 1232 } 1233 ImGui::TreePop(); 1234 } 1235 } 1236 1237 if (fShowSlidePicker) { 1238 ImGui::SetNextTreeNodeOpen(true); 1239 } 1240 1241 if (ImGui::CollapsingHeader("Slide")) { 1242 static ImGuiTextFilter filter; 1243 static ImVector<const char*> filteredSlideNames; 1244 static ImVector<int> filteredSlideIndices; 1245 1246 if (fShowSlidePicker) { 1247 ImGui::SetKeyboardFocusHere(); 1248 fShowSlidePicker = false; 1249 } 1250 1251 filter.Draw(); 1252 filteredSlideNames.clear(); 1253 filteredSlideIndices.clear(); 1254 int filteredIndex = 0; 1255 for (int i = 0; i < fSlides.count(); ++i) { 1256 const char* slideName = fSlides[i]->getName().c_str(); 1257 if (filter.PassFilter(slideName) || i == fCurrentSlide) { 1258 if (i == fCurrentSlide) { 1259 filteredIndex = filteredSlideIndices.size(); 1260 } 1261 filteredSlideNames.push_back(slideName); 1262 filteredSlideIndices.push_back(i); 1263 } 1264 } 1265 1266 if (ImGui::ListBox("", &filteredIndex, filteredSlideNames.begin(), 1267 filteredSlideNames.size(), 20)) { 1268 this->setCurrentSlide(filteredSlideIndices[filteredIndex]); 1269 } 1270 } 1271 1272 if (ImGui::CollapsingHeader("Color Mode")) { 1273 ColorMode newMode = fColorMode; 1274 auto cmButton = [&](ColorMode mode, const char* label) { 1275 if (ImGui::RadioButton(label, mode == fColorMode)) { 1276 newMode = mode; 1277 } 1278 }; 1279 1280 cmButton(ColorMode::kLegacy, "Legacy 8888"); 1281 cmButton(ColorMode::kColorManagedSRGB8888_NonLinearBlending, 1282 "Color Managed 8888 (Nonlinear blending)"); 1283 cmButton(ColorMode::kColorManagedSRGB8888, "Color Managed 8888"); 1284 cmButton(ColorMode::kColorManagedLinearF16, "Color Managed F16"); 1285 1286 if (newMode != fColorMode) { 1287 // It isn't safe to switch color mode now (in the middle of painting). We might 1288 // tear down the back-end, etc... Defer this change until the next onIdle. 1289 fDeferredActions.push_back([=]() { 1290 this->setColorMode(newMode); 1291 }); 1292 } 1293 1294 // Pick from common gamuts: 1295 int primariesIdx = 4; // Default: Custom 1296 for (size_t i = 0; i < SK_ARRAY_COUNT(gNamedPrimaries); ++i) { 1297 if (primaries_equal(*gNamedPrimaries[i].fPrimaries, fColorSpacePrimaries)) { 1298 primariesIdx = i; 1299 break; 1300 } 1301 } 1302 1303 // When we're in xform canvas mode, we can alter the transfer function, too 1304 if (ColorMode::kColorManagedSRGB8888_NonLinearBlending == fColorMode) { 1305 ImGui::SliderFloat("Gamma", &fColorSpaceTransferFn.fG, 0.5f, 3.5f); 1306 } 1307 1308 if (ImGui::Combo("Primaries", &primariesIdx, 1309 "sRGB\0AdobeRGB\0P3\0Rec. 2020\0Custom\0\0")) { 1310 if (primariesIdx >= 0 && primariesIdx <= 3) { 1311 fColorSpacePrimaries = *gNamedPrimaries[primariesIdx].fPrimaries; 1312 } 1313 } 1314 1315 // Allow direct editing of gamut 1316 ImGui_Primaries(&fColorSpacePrimaries, &fImGuiGamutPaint); 1317 } 1318 } 1319 if (paramsChanged) { 1320 fDeferredActions.push_back([=]() { 1321 fWindow->setRequestedDisplayParams(params); 1322 fWindow->inval(); 1323 this->updateTitle(); 1324 }); 1325 } 1326 ImGui::End(); 1327 } 1328 1329 if (fShowZoomWindow && fLastImage) { 1330 if (ImGui::Begin("Zoom", &fShowZoomWindow, ImVec2(200, 200))) { 1331 static int zoomFactor = 8; 1332 if (ImGui::Button("<<")) { 1333 zoomFactor = SkTMax(zoomFactor / 2, 4); 1334 } 1335 ImGui::SameLine(); ImGui::Text("%2d", zoomFactor); ImGui::SameLine(); 1336 if (ImGui::Button(">>")) { 1337 zoomFactor = SkTMin(zoomFactor * 2, 32); 1338 } 1339 1340 ImVec2 mousePos = ImGui::GetMousePos(); 1341 ImVec2 avail = ImGui::GetContentRegionAvail(); 1342 1343 uint32_t pixel = 0; 1344 SkImageInfo info = SkImageInfo::MakeN32Premul(1, 1); 1345 if (fLastImage->readPixels(info, &pixel, info.minRowBytes(), mousePos.x, mousePos.y)) { 1346 ImGui::SameLine(); 1347 ImGui::Text("(X, Y): %d, %d RGBA: %x %x %x %x", 1348 sk_float_round2int(mousePos.x), sk_float_round2int(mousePos.y), 1349 SkGetPackedR32(pixel), SkGetPackedG32(pixel), 1350 SkGetPackedB32(pixel), SkGetPackedA32(pixel)); 1351 } 1352 1353 fImGuiLayer.skiaWidget(avail, [=](SkCanvas* c) { 1354 // Translate so the region of the image that's under the mouse cursor is centered 1355 // in the zoom canvas: 1356 c->scale(zoomFactor, zoomFactor); 1357 c->translate(avail.x * 0.5f / zoomFactor - mousePos.x - 0.5f, 1358 avail.y * 0.5f / zoomFactor - mousePos.y - 0.5f); 1359 c->drawImage(this->fLastImage, 0, 0); 1360 1361 SkPaint outline; 1362 outline.setStyle(SkPaint::kStroke_Style); 1363 c->drawRect(SkRect::MakeXYWH(mousePos.x, mousePos.y, 1, 1), outline); 1364 }); 1365 } 1366 1367 ImGui::End(); 1368 } 1369} 1370 1371void Viewer::onIdle() { 1372 for (int i = 0; i < fDeferredActions.count(); ++i) { 1373 fDeferredActions[i](); 1374 } 1375 fDeferredActions.reset(); 1376 1377 fStatsLayer.beginTiming(fAnimateTimer); 1378 fAnimTimer.updateTime(); 1379 bool animateWantsInval = fSlides[fCurrentSlide]->animate(fAnimTimer); 1380 fStatsLayer.endTiming(fAnimateTimer); 1381 1382 ImGuiIO& io = ImGui::GetIO(); 1383 if (animateWantsInval || fStatsLayer.getActive() || fRefresh || io.MetricsActiveWindows) { 1384 fWindow->inval(); 1385 } 1386} 1387 1388void Viewer::updateUIState() { 1389 if (!fWindow) { 1390 return; 1391 } 1392 if (fWindow->sampleCount() < 1) { 1393 return; // Surface hasn't been created yet. 1394 } 1395 1396 // Slide state 1397 Json::Value slideState(Json::objectValue); 1398 slideState[kName] = kSlideStateName; 1399 slideState[kValue] = fSlides[fCurrentSlide]->getName().c_str(); 1400 if (fAllSlideNames.size() == 0) { 1401 for(auto slide : fSlides) { 1402 fAllSlideNames.append(Json::Value(slide->getName().c_str())); 1403 } 1404 } 1405 slideState[kOptions] = fAllSlideNames; 1406 1407 // Backend state 1408 Json::Value backendState(Json::objectValue); 1409 backendState[kName] = kBackendStateName; 1410 backendState[kValue] = kBackendTypeStrings[fBackendType]; 1411 backendState[kOptions] = Json::Value(Json::arrayValue); 1412 for (auto str : kBackendTypeStrings) { 1413 backendState[kOptions].append(Json::Value(str)); 1414 } 1415 1416 // MSAA state 1417 Json::Value msaaState(Json::objectValue); 1418 msaaState[kName] = kMSAAStateName; 1419 msaaState[kValue] = fWindow->sampleCount(); 1420 msaaState[kOptions] = Json::Value(Json::arrayValue); 1421 if (sk_app::Window::kRaster_BackendType == fBackendType) { 1422 msaaState[kOptions].append(Json::Value(0)); 1423 } else { 1424 for (int msaa : {0, 4, 8, 16}) { 1425 msaaState[kOptions].append(Json::Value(msaa)); 1426 } 1427 } 1428 1429 // Path renderer state 1430 GpuPathRenderers pr = fWindow->getRequestedDisplayParams().fGrContextOptions.fGpuPathRenderers; 1431 Json::Value prState(Json::objectValue); 1432 prState[kName] = kPathRendererStateName; 1433 prState[kValue] = gPathRendererNames[pr]; 1434 prState[kOptions] = Json::Value(Json::arrayValue); 1435 const GrContext* ctx = fWindow->getGrContext(); 1436 if (!ctx) { 1437 prState[kOptions].append("Software"); 1438 } else if (fWindow->sampleCount() > 1) { 1439 prState[kOptions].append(gPathRendererNames[GpuPathRenderers::kDefault]); 1440 prState[kOptions].append(gPathRendererNames[GpuPathRenderers::kAll]); 1441 if (ctx->caps()->shaderCaps()->pathRenderingSupport()) { 1442 prState[kOptions].append(gPathRendererNames[GpuPathRenderers::kStencilAndCover]); 1443 } 1444 if (ctx->caps()->sampleShadingSupport()) { 1445 prState[kOptions].append(gPathRendererNames[GpuPathRenderers::kMSAA]); 1446 } 1447 prState[kOptions].append(gPathRendererNames[GpuPathRenderers::kTessellating]); 1448 prState[kOptions].append(gPathRendererNames[GpuPathRenderers::kNone]); 1449 } else { 1450 prState[kOptions].append(gPathRendererNames[GpuPathRenderers::kDefault]); 1451 prState[kOptions].append(gPathRendererNames[GpuPathRenderers::kAll]); 1452 if (GrCoverageCountingPathRenderer::IsSupported(*ctx->caps())) { 1453 prState[kOptions].append(gPathRendererNames[GpuPathRenderers::kCoverageCounting]); 1454 } 1455 prState[kOptions].append(gPathRendererNames[GpuPathRenderers::kSmall]); 1456 prState[kOptions].append(gPathRendererNames[GpuPathRenderers::kTessellating]); 1457 prState[kOptions].append(gPathRendererNames[GpuPathRenderers::kNone]); 1458 } 1459 1460 // Softkey state 1461 Json::Value softkeyState(Json::objectValue); 1462 softkeyState[kName] = kSoftkeyStateName; 1463 softkeyState[kValue] = kSoftkeyHint; 1464 softkeyState[kOptions] = Json::Value(Json::arrayValue); 1465 softkeyState[kOptions].append(kSoftkeyHint); 1466 for (const auto& softkey : fCommands.getCommandsAsSoftkeys()) { 1467 softkeyState[kOptions].append(Json::Value(softkey.c_str())); 1468 } 1469 1470 // FPS state 1471 Json::Value fpsState(Json::objectValue); 1472 fpsState[kName] = kFpsStateName; 1473 double animTime = fStatsLayer.getLastTime(fAnimateTimer); 1474 double paintTime = fStatsLayer.getLastTime(fPaintTimer); 1475 double flushTime = fStatsLayer.getLastTime(fFlushTimer); 1476 fpsState[kValue] = SkStringPrintf("%8.3lf ms\n\nA %8.3lf\nP %8.3lf\nF%8.3lf", 1477 animTime + paintTime + flushTime, 1478 animTime, paintTime, flushTime).c_str(); 1479 fpsState[kOptions] = Json::Value(Json::arrayValue); 1480 1481 Json::Value state(Json::arrayValue); 1482 state.append(slideState); 1483 state.append(backendState); 1484 state.append(msaaState); 1485 state.append(prState); 1486 state.append(softkeyState); 1487 state.append(fpsState); 1488 1489 fWindow->setUIState(state.toStyledString().c_str()); 1490} 1491 1492void Viewer::onUIStateChanged(const SkString& stateName, const SkString& stateValue) { 1493 // For those who will add more features to handle the state change in this function: 1494 // After the change, please call updateUIState no notify the frontend (e.g., Android app). 1495 // For example, after slide change, updateUIState is called inside setupCurrentSlide; 1496 // after backend change, updateUIState is called in this function. 1497 if (stateName.equals(kSlideStateName)) { 1498 for (int i = 0; i < fSlides.count(); ++i) { 1499 if (fSlides[i]->getName().equals(stateValue)) { 1500 this->setCurrentSlide(i); 1501 return; 1502 } 1503 } 1504 1505 SkDebugf("Slide not found: %s", stateValue.c_str()); 1506 } else if (stateName.equals(kBackendStateName)) { 1507 for (int i = 0; i < sk_app::Window::kBackendTypeCount; i++) { 1508 if (stateValue.equals(kBackendTypeStrings[i])) { 1509 if (fBackendType != i) { 1510 fBackendType = (sk_app::Window::BackendType)i; 1511 fWindow->detach(); 1512 fWindow->attach(backend_type_for_window(fBackendType)); 1513 } 1514 break; 1515 } 1516 } 1517 } else if (stateName.equals(kMSAAStateName)) { 1518 DisplayParams params = fWindow->getRequestedDisplayParams(); 1519 int sampleCount = atoi(stateValue.c_str()); 1520 if (sampleCount != params.fMSAASampleCount) { 1521 params.fMSAASampleCount = sampleCount; 1522 fWindow->setRequestedDisplayParams(params); 1523 fWindow->inval(); 1524 this->updateTitle(); 1525 this->updateUIState(); 1526 } 1527 } else if (stateName.equals(kPathRendererStateName)) { 1528 DisplayParams params = fWindow->getRequestedDisplayParams(); 1529 for (const auto& pair : gPathRendererNames) { 1530 if (pair.second == stateValue.c_str()) { 1531 if (params.fGrContextOptions.fGpuPathRenderers != pair.first) { 1532 params.fGrContextOptions.fGpuPathRenderers = pair.first; 1533 fWindow->setRequestedDisplayParams(params); 1534 fWindow->inval(); 1535 this->updateTitle(); 1536 this->updateUIState(); 1537 } 1538 break; 1539 } 1540 } 1541 } else if (stateName.equals(kSoftkeyStateName)) { 1542 if (!stateValue.equals(kSoftkeyHint)) { 1543 fCommands.onSoftkey(stateValue); 1544 this->updateUIState(); // This is still needed to reset the value to kSoftkeyHint 1545 } 1546 } else if (stateName.equals(kRefreshStateName)) { 1547 // This state is actually NOT in the UI state. 1548 // We use this to allow Android to quickly set bool fRefresh. 1549 fRefresh = stateValue.equals(kON); 1550 } else { 1551 SkDebugf("Unknown stateName: %s", stateName.c_str()); 1552 } 1553} 1554 1555bool Viewer::onKey(sk_app::Window::Key key, sk_app::Window::InputState state, uint32_t modifiers) { 1556 return fCommands.onKey(key, state, modifiers); 1557} 1558 1559bool Viewer::onChar(SkUnichar c, uint32_t modifiers) { 1560 if (fSlides[fCurrentSlide]->onChar(c)) { 1561 fWindow->inval(); 1562 return true; 1563 } else { 1564 return fCommands.onChar(c, modifiers); 1565 } 1566} 1567