| #include "fiddle_context.hpp" | |
| #include "rive/math/simd.hpp" | |
| #include "rive/artboard.hpp" | |
| #include "rive/file.hpp" | |
| #include "rive/refcnt.hpp" | |
| #include "rive/layout.hpp" | |
| #include "rive/animation/state_machine_instance.hpp" | |
| #include "rive/static_scene.hpp" | |
| #include "rive/profiler/profiler_macros.h" | |
| #include <filesystem> | |
| #include <fstream> | |
| #include <iterator> | |
| #include <vector> | |
| #include <sstream> | |
| #ifdef _WIN32 | |
| #include <windows.h> | |
| #endif | |
| #define GLFW_INCLUDE_NONE | |
| #ifdef _WIN32 | |
| #define GLFW_EXPOSE_NATIVE_WIN32 | |
| #endif | |
| #include "GLFW/glfw3.h" | |
| #ifdef _WIN32 | |
| #include <GLFW/glfw3native.h> | |
| #endif | |
| #ifdef _WIN32 | |
| // Windows only lets a process take the foreground if it already owns it, | |
| // or was started by whoever does. Launching from a terminal usually | |
| // satisfies neither, so SetForegroundWindow() is silently ignored and the | |
| // window opens behind the shell. AttachThreadInput() makes the two threads | |
| // share a single input state, which is the attachment SetForegroundWindow() | |
| // looks for. The sharing is mutual, so the argument order is immaterial -- | |
| // what matters is that the detach repeats the attach. | |
| static void forceWindowToForeground(GLFWwindow* window) | |
| { | |
| HWND hwnd = glfwGetWin32Window(window); | |
| HWND foreground = GetForegroundWindow(); | |
| DWORD foregroundThread = | |
| foreground ? GetWindowThreadProcessId(foreground, nullptr) : 0; | |
| DWORD thisThread = GetCurrentThreadId(); | |
| bool attached = foregroundThread != 0 && foregroundThread != thisThread && | |
| AttachThreadInput(foregroundThread, thisThread, TRUE); | |
| BringWindowToTop(hwnd); | |
| SetForegroundWindow(hwnd); | |
| SetFocus(hwnd); | |
| if (attached) | |
| { | |
| AttachThreadInput(foregroundThread, thisThread, FALSE); | |
| } | |
| } | |
| #endif | |
| #ifdef __EMSCRIPTEN__ | |
| #include <emscripten/emscripten.h> | |
| #include <emscripten/html5.h> | |
| #include <sstream> | |
| #endif | |
| using namespace rive; | |
| constexpr static char kMoltenVKICD[] = | |
| "dependencies/MoltenVK/Package/Release/MoltenVK/dynamic/dylib/macOS/" | |
| "MoltenVK_icd.json"; | |
| constexpr static char kSwiftShaderICD[] = "dependencies/SwiftShader/build/" | |
| #ifdef __APPLE__ | |
| "Darwin" | |
| #elif defined(_WIN32) | |
| "Windows" | |
| #else | |
| "Linux" | |
| #endif | |
| "/vk_swiftshader_icd.json"; | |
| #ifdef _WIN32 | |
| extern "C" | |
| { | |
| // https://stackoverflow.com/questions/68469954/how-to-choose-specific-gpu-when-create-opengl-context: | |
| // | |
| // OpenGL, or rather the Win32 GDI integration of it, doesn't offer means | |
| // to explicitly select the desired device. However the drivers of Nvidia | |
| // and AMD offer a workaround to have programs select, that they prefer to | |
| // execute on the discrete GPU rather than the CPU integrated one. | |
| // | |
| // These also appear to select the discrete "Arc" GPU on an Intel system, | |
| // and to influence the GPU selection on D3D11. | |
| __declspec(dllexport) uint32_t NvOptimusEnablement = 1; | |
| __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; | |
| } | |
| #endif | |
| static FiddleContextOptions options; | |
| static GLFWwindow* window = nullptr; | |
| static uint32_t msaa = 0; | |
| static bool forceAtomicMode = false; | |
| static bool wireframe = false; | |
| static bool disableFill = false; | |
| static bool disableStroke = false; | |
| static bool clockwiseFill = false; | |
| static bool hotloadShaders = false; | |
| static std::unique_ptr<FiddleContext> fiddleContext; | |
| static float2 pts[] = {{260 + 2 * 100, 60 + 2 * 500}, | |
| {260 + 2 * 257, 60 + 2 * 233}, | |
| {260 + 2 * -100, 60 + 2 * 300}, | |
| {260 + 2 * 100, 60 + 2 * 200}, | |
| {260 + 2 * 250, 60 + 2 * 0}, | |
| {260 + 2 * 400, 60 + 2 * 200}, | |
| {260 + 2 * 213, 60 + 2 * 200}, | |
| {260 + 2 * 213, 60 + 2 * 300}, | |
| {260 + 2 * 391, 60 + 2 * 480}, | |
| {1400, 1400}}; // Feather control. | |
| constexpr static int NUM_INTERACTIVE_PTS = sizeof(pts) / sizeof(*pts); | |
| static float strokeWidth = 70; | |
| static float2 translate; | |
| static float scale = 1; | |
| static StrokeJoin join = StrokeJoin::round; | |
| static StrokeCap cap = StrokeCap::round; | |
| static bool doClose = false; | |
| static bool paused = false; | |
| static bool unlockedLogic = true; | |
| static int dragIdx = -1; | |
| static float2 dragLastPos; | |
| static int animation = -1; | |
| static int stateMachine = -1; | |
| static int horzRepeat = 0; | |
| static int upRepeat = 0; | |
| static int downRepeat = 0; | |
| rcp<File> rivFile; | |
| std::vector<std::unique_ptr<Artboard>> artboards; | |
| std::vector<std::unique_ptr<Scene>> scenes; | |
| std::vector<rive::rcp<rive::ViewModelInstance>> viewModelInstances; | |
| static void clear_scenes() | |
| { | |
| artboards.clear(); | |
| scenes.clear(); | |
| viewModelInstances.clear(); | |
| } | |
| static void make_scenes(size_t count) | |
| { | |
| clear_scenes(); | |
| for (size_t i = 0; i < count; ++i) | |
| { | |
| // Tip: you can change the artboard shown here | |
| // auto artboard = rivFile->artboardAt(2); | |
| auto artboard = rivFile->artboardDefault(); | |
| std::unique_ptr<Scene> scene; | |
| if (stateMachine >= 0) | |
| { | |
| scene = artboard->stateMachineAt(stateMachine); | |
| } | |
| else if (animation >= 0) | |
| { | |
| scene = artboard->animationAt(animation); | |
| } | |
| else | |
| { | |
| scene = artboard->animationAt(0); | |
| } | |
| if (scene == nullptr) | |
| { | |
| // This is a riv without any animations or state machines. Just draw | |
| // the artboard. | |
| scene = std::make_unique<StaticScene>(artboard.get()); | |
| } | |
| int viewModelId = artboard.get()->viewModelId(); | |
| viewModelInstances.push_back( | |
| viewModelId == -1 | |
| ? rivFile->createViewModelInstance(artboard.get()) | |
| : rivFile->createViewModelInstance(viewModelId, 0)); | |
| artboard->bindViewModelInstance(viewModelInstances.back()); | |
| if (viewModelInstances.back() != nullptr) | |
| { | |
| scene->bindViewModelInstance(viewModelInstances.back()); | |
| } | |
| scene->advanceAndApply(scene->durationSeconds() * i / count); | |
| artboards.push_back(std::move(artboard)); | |
| scenes.push_back(std::move(scene)); | |
| } | |
| } | |
| #ifdef __EMSCRIPTEN__ | |
| EM_JS(int, window_inner_width, (), { return window["innerWidth"]; }); | |
| EM_JS(int, window_inner_height, (), { return window["innerHeight"]; }); | |
| EM_JS(char*, get_location_hash_str, (), { | |
| var jsString = window.location.hash.substring(1); | |
| var lengthBytes = lengthBytesUTF8(jsString) + 1; | |
| var stringOnWasmHeap = _malloc(lengthBytes); | |
| stringToUTF8(jsString, stringOnWasmHeap, lengthBytes); | |
| return stringOnWasmHeap; | |
| }); | |
| #endif | |
| static void mouse_button_callback(GLFWwindow* window, | |
| int button, | |
| int action, | |
| int mods) | |
| { | |
| double x, y; | |
| glfwGetCursorPos(window, &x, &y); | |
| float dpiScale = fiddleContext->dpiScale(window); | |
| x *= dpiScale; | |
| y *= dpiScale; | |
| dragLastPos = float2{(float)x, (float)y}; | |
| if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_PRESS) | |
| { | |
| dragIdx = -1; | |
| if (!rivFile) | |
| { | |
| for (int i = 0; i < NUM_INTERACTIVE_PTS; ++i) | |
| { | |
| if (simd::all(simd::abs(dragLastPos - (pts[i] + translate)) < | |
| 100)) | |
| { | |
| dragIdx = i; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| static void mousemove_callback(GLFWwindow* window, double x, double y) | |
| { | |
| float dpiScale = fiddleContext->dpiScale(window); | |
| x *= dpiScale; | |
| y *= dpiScale; | |
| if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) | |
| { | |
| float2 pos = float2{(float)x, (float)y}; | |
| if (dragIdx >= 0) | |
| { | |
| pts[dragIdx] += (pos - dragLastPos); | |
| } | |
| else | |
| { | |
| translate += (pos - dragLastPos); | |
| } | |
| dragLastPos = pos; | |
| } | |
| } | |
| int lastWidth = 0, lastHeight = 0; | |
| double fpsLastTime = 0; | |
| double fpsLastTimeLogic = 0; | |
| int fpsFrames = 0; | |
| static bool needsTitleUpdate = false; | |
| enum class API | |
| { | |
| gl, | |
| metal, | |
| d3d, | |
| d3d12, | |
| dawn, | |
| vulkan, | |
| }; | |
| API api = | |
| #if defined(__APPLE__) | |
| API::metal | |
| #elif defined(_WIN32) | |
| API::d3d | |
| #else | |
| API::gl | |
| #endif | |
| ; | |
| bool angle = false; | |
| bool skia = false; | |
| static void key_callback(GLFWwindow* window, | |
| int key, | |
| int scancode, | |
| int action, | |
| int mods) | |
| { | |
| bool shift = mods & GLFW_MOD_SHIFT; | |
| if (action == GLFW_PRESS) | |
| { | |
| switch (key) | |
| { | |
| case GLFW_KEY_ESCAPE: | |
| glfwSetWindowShouldClose(window, 1); | |
| break; | |
| case GLFW_KEY_GRAVE_ACCENT: // ` key, backtick | |
| hotloadShaders = true; | |
| break; | |
| case GLFW_KEY_A: | |
| forceAtomicMode = !forceAtomicMode; | |
| fpsLastTime = 0; | |
| fpsFrames = 0; | |
| needsTitleUpdate = true; | |
| break; | |
| case GLFW_KEY_D: | |
| printf("static float scale = %f;\n", scale); | |
| printf("static float2 translate = {%f, %f};\n", | |
| translate.x, | |
| translate.y); | |
| printf("static float2 pts[] = {"); | |
| for (int i = 0; i < NUM_INTERACTIVE_PTS; i++) | |
| { | |
| printf("{%g, %g}", pts[i].x, pts[i].y); | |
| if (i < NUM_INTERACTIVE_PTS - 1) | |
| { | |
| printf(", "); | |
| } | |
| else | |
| { | |
| printf("};\n"); | |
| } | |
| } | |
| fflush(stdout); | |
| break; | |
| case GLFW_KEY_Z: | |
| fiddleContext->toggleZoomWindow(); | |
| break; | |
| case GLFW_KEY_MINUS: | |
| strokeWidth /= 1.5f; | |
| break; | |
| case GLFW_KEY_EQUAL: | |
| strokeWidth *= 1.5f; | |
| break; | |
| case GLFW_KEY_W: | |
| wireframe = !wireframe; | |
| break; | |
| case GLFW_KEY_C: | |
| cap = static_cast<StrokeCap>((static_cast<int>(cap) + 1) % 3); | |
| break; | |
| case GLFW_KEY_O: | |
| doClose = !doClose; | |
| break; | |
| case GLFW_KEY_S: | |
| if (shift) | |
| { | |
| // Toggle Skia. | |
| clear_scenes(); | |
| rivFile = nullptr; | |
| skia = !skia; | |
| fiddleContext = skia ? FiddleContext::MakeGLSkia() | |
| : FiddleContext::MakeGLPLS(); | |
| lastWidth = 0; | |
| lastHeight = 0; | |
| fpsLastTime = 0; | |
| fpsFrames = 0; | |
| needsTitleUpdate = true; | |
| } | |
| else | |
| { | |
| disableStroke = !disableStroke; | |
| } | |
| break; | |
| case GLFW_KEY_F: | |
| disableFill = !disableFill; | |
| break; | |
| case GLFW_KEY_X: | |
| clockwiseFill = !clockwiseFill; | |
| break; | |
| case GLFW_KEY_P: | |
| if (!shift) | |
| { | |
| paused = !paused; | |
| } | |
| else | |
| { | |
| RIVE_PROF_TOGGLEDRAW(); | |
| } | |
| break; | |
| case GLFW_KEY_H: | |
| if (!shift) | |
| ++horzRepeat; | |
| else if (horzRepeat > 0) | |
| --horzRepeat; | |
| break; | |
| case GLFW_KEY_K: | |
| if (!shift) | |
| ++upRepeat; | |
| else if (upRepeat > 0) | |
| --upRepeat; | |
| break; | |
| case GLFW_KEY_J: | |
| if (!rivFile) | |
| join = static_cast<StrokeJoin>( | |
| (static_cast<int>(join) + 1) % 3); | |
| else if (!shift) | |
| ++downRepeat; | |
| else if (downRepeat > 0) | |
| --downRepeat; | |
| break; | |
| case GLFW_KEY_UP: | |
| { | |
| float oldScale = scale; | |
| scale *= 1.25; | |
| double x = 0, y = 0; | |
| glfwGetCursorPos(window, &x, &y); | |
| float2 cursorPos = float2{(float)x, (float)y} * | |
| fiddleContext->dpiScale(window); | |
| translate = | |
| cursorPos + (translate - cursorPos) * scale / oldScale; | |
| break; | |
| } | |
| case GLFW_KEY_DOWN: | |
| { | |
| float oldScale = scale; | |
| scale /= 1.25; | |
| double x = 0, y = 0; | |
| glfwGetCursorPos(window, &x, &y); | |
| float2 cursorPos = float2{(float)x, (float)y} * | |
| fiddleContext->dpiScale(window); | |
| translate = | |
| cursorPos + (translate - cursorPos) * scale / oldScale; | |
| break; | |
| } | |
| case GLFW_KEY_U: | |
| unlockedLogic = !unlockedLogic; | |
| } | |
| } | |
| } | |
| static void glfw_error_callback(int code, const char* message) | |
| { | |
| printf("GLFW error: %i - %s\n", code, message); | |
| } | |
| static void set_environment_variable(const char* name, const char* value) | |
| { | |
| if (const char* existingValue = getenv(name)) | |
| { | |
| printf("warning: %s=%s already set. Overriding with %s=%s\n", | |
| name, | |
| existingValue, | |
| name, | |
| value); | |
| } | |
| #ifdef _WIN32 | |
| SetEnvironmentVariableA(name, value); | |
| #else | |
| setenv(name, value, /*overwrite=*/true); | |
| #endif | |
| } | |
| std::unique_ptr<Renderer> renderer; | |
| std::string rivName; | |
| void riveMainLoop(); | |
| int main(int argc, const char** argv) | |
| { | |
| // Cause stdout and stderr to print immediately without buffering. | |
| setvbuf(stdout, NULL, _IONBF, 0); | |
| setvbuf(stderr, NULL, _IONBF, 0); | |
| RIVE_PROF_INIT() | |
| #ifdef DEBUG | |
| options.enableVulkanCoreValidationLayers = true; | |
| #endif | |
| #ifdef __EMSCRIPTEN__ | |
| emscripten_set_main_loop(riveMainLoop, 0, false); | |
| // Override argc/argv with the window location hash string. | |
| char* hash = get_location_hash_str(); | |
| std::stringstream ss(hash); | |
| std::vector<std::string> hashStrs; | |
| std::vector<const char*> hashArgs; | |
| std::string arg; | |
| hashStrs.push_back("index.html"); | |
| while (std::getline(ss, arg, ':')) | |
| { | |
| hashStrs.push_back(std::move(arg)); | |
| } | |
| for (const std::string& str : hashStrs) | |
| { | |
| hashArgs.push_back(str.c_str()); | |
| } | |
| argc = hashArgs.size(); | |
| argv = hashArgs.data(); | |
| free(hash); | |
| #endif | |
| for (int i = 1; i < argc; i++) | |
| { | |
| if (!strcmp(argv[i], "--gl")) | |
| { | |
| api = API::gl; | |
| } | |
| else if (!strcmp(argv[i], "--glatomic")) | |
| { | |
| api = API::gl; | |
| forceAtomicMode = true; | |
| } | |
| else if (!strcmp(argv[i], "--glcw")) | |
| { | |
| api = API::gl; | |
| clockwiseFill = true; | |
| } | |
| else if (!strcmp(argv[i], "--metal")) | |
| { | |
| api = API::metal; | |
| } | |
| else if (!strcmp(argv[i], "--metalcw")) | |
| { | |
| api = API::metal; | |
| clockwiseFill = true; | |
| } | |
| else if (!strcmp(argv[i], "--metalatomic")) | |
| { | |
| api = API::metal; | |
| forceAtomicMode = true; | |
| } | |
| else if (!strcmp(argv[i], "--mvk") || !strcmp(argv[i], "--moltenvk")) | |
| { | |
| set_environment_variable("VK_ICD_FILENAMES", kMoltenVKICD); | |
| api = API::vulkan; | |
| } | |
| else if (!strcmp(argv[i], "--mvkatomic") || | |
| !strcmp(argv[i], "--moltenvkatomic")) | |
| { | |
| set_environment_variable("VK_ICD_FILENAMES", kMoltenVKICD); | |
| api = API::vulkan; | |
| forceAtomicMode = true; | |
| } | |
| else if (!strcmp(argv[i], "--sw") || !strcmp(argv[i], "--swiftshader")) | |
| { | |
| // Use the swiftshader built by | |
| // packages/runtime/renderer/make_swiftshader.sh | |
| set_environment_variable("VK_ICD_FILENAMES", kSwiftShaderICD); | |
| api = API::vulkan; | |
| } | |
| else if (!strcmp(argv[i], "--swatomic") || | |
| !strcmp(argv[i], "--swiftshaderatomic")) | |
| { | |
| // Use the swiftshader built by | |
| // packages/runtime/renderer/make_swiftshader.sh | |
| set_environment_variable("VK_ICD_FILENAMES", kSwiftShaderICD); | |
| api = API::vulkan; | |
| forceAtomicMode = true; | |
| } | |
| else if (!strcmp(argv[i], "--dawn")) | |
| { | |
| api = API::dawn; | |
| } | |
| else if (!strcmp(argv[i], "--d3d")) | |
| { | |
| api = API::d3d; | |
| } | |
| else if (!strcmp(argv[i], "--d3d12")) | |
| { | |
| api = API::d3d12; | |
| } | |
| else if (!strcmp(argv[i], "--d3datomic")) | |
| { | |
| api = API::d3d; | |
| forceAtomicMode = true; | |
| } | |
| else if (!strcmp(argv[i], "--d3d12atomic")) | |
| { | |
| api = API::d3d12; | |
| forceAtomicMode = true; | |
| } | |
| else if (!strcmp(argv[i], "--vulkan") || !strcmp(argv[i], "--vk")) | |
| { | |
| api = API::vulkan; | |
| } | |
| else if (!strcmp(argv[i], "--vkcw")) | |
| { | |
| api = API::vulkan; | |
| clockwiseFill = true; | |
| } | |
| else if (!strcmp(argv[i], "--vkcwa")) | |
| { | |
| api = API::vulkan; | |
| clockwiseFill = true; | |
| forceAtomicMode = true; | |
| } | |
| else if (!strcmp(argv[i], "--vulkanatomic") || | |
| !strcmp(argv[i], "--vkatomic")) | |
| { | |
| api = API::vulkan; | |
| forceAtomicMode = true; | |
| } | |
| #ifdef RIVE_DESKTOP_GL | |
| else if (!strcmp(argv[i], "--angle_gl")) | |
| { | |
| api = API::gl; | |
| glfwInitHint(GLFW_ANGLE_PLATFORM_TYPE, | |
| GLFW_ANGLE_PLATFORM_TYPE_OPENGL); | |
| angle = true; | |
| } | |
| else if (!strcmp(argv[i], "--angle_d3d")) | |
| { | |
| api = API::gl; | |
| glfwInitHint(GLFW_ANGLE_PLATFORM_TYPE, | |
| GLFW_ANGLE_PLATFORM_TYPE_D3D11); | |
| angle = true; | |
| } | |
| else if (!strcmp(argv[i], "--angle_vk")) | |
| { | |
| api = API::gl; | |
| glfwInitHint(GLFW_ANGLE_PLATFORM_TYPE, | |
| GLFW_ANGLE_PLATFORM_TYPE_VULKAN); | |
| angle = true; | |
| } | |
| else if (!strcmp(argv[i], "--angle_mtl")) | |
| { | |
| api = API::gl; | |
| glfwInitHint(GLFW_ANGLE_PLATFORM_TYPE, | |
| GLFW_ANGLE_PLATFORM_TYPE_METAL); | |
| angle = true; | |
| } | |
| #endif | |
| else if (!strcmp(argv[i], "--skia")) | |
| { | |
| skia = true; | |
| } | |
| else if (sscanf(argv[i], "-a%i", &animation) == 1) | |
| { | |
| // Already updated animation. | |
| } | |
| else if (sscanf(argv[i], "-s%i", &stateMachine) == 1) | |
| { | |
| // Already updated stateMachine. | |
| } | |
| else if (sscanf(argv[i], "-h%i", &horzRepeat) == 1) | |
| { | |
| // Already updated horzRepeat. | |
| } | |
| else if (sscanf(argv[i], "-j%i", &downRepeat) == 1) | |
| { | |
| // Already updated downRepeat. | |
| } | |
| else if (sscanf(argv[i], "-k%i", &upRepeat) == 1) | |
| { | |
| // Already updated upRepeat. | |
| } | |
| else if (!strcmp(argv[i], "-p")) | |
| { | |
| paused = true; | |
| } | |
| else if (!strcmp(argv[i], "--d3d12Warp")) | |
| { | |
| options.d3d12UseWarpDevice = true; | |
| } | |
| else if (!strcmp(argv[i], "--atomic")) | |
| { | |
| forceAtomicMode = true; | |
| } | |
| else if (!strcmp(argv[i], "--cw")) | |
| { | |
| clockwiseFill = true; | |
| } | |
| else if (sscanf(argv[i], "--msaa%u", &msaa) == 1) | |
| { | |
| // Already updated msaa | |
| } | |
| else if (!strcmp(argv[i], "--core")) | |
| { | |
| options.coreFeaturesOnly = true; | |
| } | |
| else if (!strcmp(argv[i], "--validation")) | |
| { | |
| options.enableVulkanCoreValidationLayers = true; | |
| } | |
| else if (!strcmp(argv[i], "--gpu") || !strcmp(argv[i], "-g")) | |
| { | |
| options.gpuNameFilter = argv[++i]; | |
| } | |
| else if (!strcmp(argv[i], "--integrated") || !strcmp(argv[i], "-i")) | |
| { | |
| options.gpuNameFilter = "integrated"; | |
| } | |
| else if (!strncmp(argv[i], "--", 2)) | |
| { | |
| fprintf(stderr, "Unknown command-line option %s\n", argv[i]); | |
| return 1; | |
| } | |
| else | |
| { | |
| rivName = argv[i]; | |
| } | |
| } | |
| #ifdef _WIN32 | |
| // Set our backdoor GPU selection variables in case the API doesn't allow us | |
| // to select explicitly. | |
| if (options.gpuNameFilter != nullptr) | |
| { | |
| // "i" and "integrated" are special-case gpuNameFilters that mean "use | |
| // the integrated GPU". | |
| bool wantIntegratedGPU = static_cast<uint32_t>( | |
| strcmp(options.gpuNameFilter, "integrated") == 0 || | |
| strcmp(options.gpuNameFilter, "i") == 0); | |
| NvOptimusEnablement = !wantIntegratedGPU; | |
| AmdPowerXpressRequestHighPerformance = !wantIntegratedGPU; | |
| } | |
| #endif | |
| glfwSetErrorCallback(glfw_error_callback); | |
| if (!glfwInit()) | |
| { | |
| fprintf(stderr, "Failed to initialize glfw.\n"); | |
| return 1; | |
| } | |
| if (msaa > 0) | |
| { | |
| if (msaa > 1) | |
| { | |
| glfwWindowHint(GLFW_SAMPLES, msaa); | |
| } | |
| glfwWindowHint(GLFW_STENCIL_BITS, 8); | |
| glfwWindowHint(GLFW_DEPTH_BITS, 16); | |
| } | |
| glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); | |
| switch (api) | |
| { | |
| case API::metal: | |
| case API::d3d: | |
| case API::d3d12: | |
| case API::dawn: | |
| glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); | |
| glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW_TRUE); | |
| break; | |
| case API::vulkan: | |
| glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); | |
| break; | |
| case API::gl: | |
| if (angle) | |
| { | |
| glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API); | |
| glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); | |
| glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); | |
| glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); | |
| glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); | |
| } | |
| else | |
| { | |
| glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API); | |
| glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); | |
| glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6); | |
| } | |
| break; | |
| } | |
| // Create hidden, then show. GLFW_FOCUS_ON_SHOW only applies to a | |
| // hidden->visible transition, so with the default (visible) the show | |
| // below is a no-op and the window can come up behind the terminal. | |
| glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); | |
| glfwWindowHint(GLFW_FOCUS_ON_SHOW, GLFW_TRUE); | |
| // glfwWindowHint(GLFW_FLOATING, GLFW_TRUE); | |
| window = glfwCreateWindow(1600, 1600, "Rive Renderer", nullptr, nullptr); | |
| if (!window) | |
| { | |
| glfwTerminate(); | |
| fprintf(stderr, "Failed to create window.\n"); | |
| return -1; | |
| } | |
| glfwSetMouseButtonCallback(window, mouse_button_callback); | |
| glfwSetCursorPosCallback(window, mousemove_callback); | |
| glfwSetKeyCallback(window, key_callback); | |
| if (api == API::gl) | |
| { | |
| glfwMakeContextCurrent(window); | |
| } | |
| glfwShowWindow(window); | |
| // Windows can still refuse to hand over the foreground, so ask | |
| // explicitly as well. | |
| glfwFocusWindow(window); | |
| #ifdef _WIN32 | |
| forceWindowToForeground(window); | |
| #endif | |
| switch (api) | |
| { | |
| case API::metal: | |
| fiddleContext = FiddleContext::MakeMetalPLS(options); | |
| break; | |
| case API::d3d: | |
| fiddleContext = FiddleContext::MakeD3DPLS(options); | |
| break; | |
| case API::d3d12: | |
| fiddleContext = FiddleContext::MakeD3D12PLS(options); | |
| break; | |
| case API::dawn: | |
| fiddleContext = FiddleContext::MakeDawnPLS(options); | |
| break; | |
| case API::vulkan: | |
| fiddleContext = FiddleContext::MakeVulkanPLS(options); | |
| break; | |
| case API::gl: | |
| fiddleContext = | |
| skia ? FiddleContext::MakeGLSkia() : FiddleContext::MakeGLPLS(); | |
| break; | |
| } | |
| if (!fiddleContext) | |
| { | |
| fprintf(stderr, "Failed to create a fiddle context.\n"); | |
| abort(); | |
| } | |
| #ifndef __EMSCRIPTEN__ | |
| if (api == API::gl) | |
| { | |
| glfwSwapInterval(0); | |
| } | |
| while (!glfwWindowShouldClose(window)) | |
| { | |
| riveMainLoop(); | |
| fiddleContext->tick(); | |
| if (api == API::gl) | |
| { | |
| glfwSwapBuffers(window); | |
| } | |
| if (!rivName.empty()) | |
| { | |
| glfwPollEvents(); | |
| } | |
| else | |
| { | |
| glfwWaitEvents(); | |
| } | |
| } | |
| // We need to clear the riv scene (which can be holding on to render | |
| // resources) before releasing the fiddle context | |
| clear_scenes(); | |
| rivFile = nullptr; | |
| fiddleContext = nullptr; | |
| glfwTerminate(); | |
| #endif | |
| return 0; | |
| } | |
| static void update_window_title(double fps, | |
| int instances, | |
| int width, | |
| int height) | |
| { | |
| std::ostringstream title; | |
| if (fps != 0) | |
| { | |
| title << '[' << fps << " FPS]"; | |
| } | |
| if (instances > 1) | |
| { | |
| title << " (x" << instances << " instances)"; | |
| } | |
| if (skia) | |
| { | |
| title << " | SKIA Renderer"; | |
| } | |
| else | |
| { | |
| title << " | RIVE Renderer"; | |
| } | |
| if (msaa) | |
| { | |
| title << " (msaa" << msaa << ')'; | |
| } | |
| else if (forceAtomicMode) | |
| { | |
| title << " (atomic)"; | |
| } | |
| title << " | " << width << " x " << height; | |
| glfwSetWindowTitle(window, title.str().c_str()); | |
| } | |
| void riveMainLoop() | |
| { | |
| RIVE_PROF_FRAME() | |
| RIVE_PROF_SCOPE_L(0) | |
| #ifdef __EMSCRIPTEN__ | |
| { | |
| // Fit the canvas to the browser window size. | |
| int windowWidth = window_inner_width(); | |
| int windowHeight = window_inner_height(); | |
| double devicePixelRatio = emscripten_get_device_pixel_ratio(); | |
| int canvasExpectedWidth = windowWidth * devicePixelRatio; | |
| int canvasExpectedHeight = windowHeight * devicePixelRatio; | |
| int canvasWidth, canvasHeight; | |
| glfwGetFramebufferSize(window, &canvasWidth, &canvasHeight); | |
| if (canvasWidth != canvasExpectedWidth || | |
| canvasHeight != canvasExpectedHeight) | |
| { | |
| glfwSetWindowSize(window, | |
| canvasExpectedWidth, | |
| canvasExpectedHeight); | |
| emscripten_set_element_css_size("#canvas", | |
| windowWidth, | |
| windowHeight); | |
| } | |
| } | |
| #endif | |
| int width = 0, height = 0; | |
| glfwGetFramebufferSize(window, &width, &height); | |
| if (lastWidth != width || lastHeight != height) | |
| { | |
| printf("size changed to %ix%i\n", width, height); | |
| lastWidth = width; | |
| lastHeight = height; | |
| fiddleContext->onSizeChanged(window, width, height, msaa); | |
| renderer = fiddleContext->makeRenderer(width, height); | |
| needsTitleUpdate = true; | |
| } | |
| if (needsTitleUpdate) | |
| { | |
| update_window_title(0, 1, width, height); | |
| needsTitleUpdate = false; | |
| } | |
| if (!rivName.empty() && !rivFile) | |
| { | |
| std::ifstream rivStream(rivName, std::ios::binary); | |
| std::vector<uint8_t> rivBytes(std::istreambuf_iterator<char>(rivStream), | |
| {}); | |
| rivFile = File::import(rivBytes, fiddleContext->factory()); | |
| } | |
| // Call right before begin() | |
| if (hotloadShaders) | |
| { | |
| hotloadShaders = false; | |
| #ifndef RIVE_BUILD_FOR_IOS | |
| // We want to build to /tmp/rive (or the correct equivalent) | |
| std::filesystem::path tempRiveDir = | |
| std::filesystem::temp_directory_path() / "rive"; | |
| // Get the u8 version of the path (this is especially necessary on | |
| // windows where the native path character type is wchar_t, then | |
| // reinterpret_cast the char8_t pointer to char so we can append it to | |
| // our string. | |
| // Store the u8string result to extend its lifetime (need to | |
| // reinterpret_cast through a const char * pointer because u8string | |
| // returns a std::u8string in C++20 and newer, but we need it as a | |
| // "char" string) | |
| std::string tempRiveDirStr = | |
| reinterpret_cast<const char*>(tempRiveDir.u8string().c_str()); | |
| std::string rebuildCommand = "sh rebuild_shaders.sh " + tempRiveDirStr; | |
| std::system(rebuildCommand.c_str()); | |
| #endif | |
| fiddleContext->hotloadShaders(); | |
| } | |
| fiddleContext->begin({ | |
| .renderTargetWidth = static_cast<uint32_t>(width), | |
| .renderTargetHeight = static_cast<uint32_t>(height), | |
| .clearColor = 0xff303030, | |
| .msaaSampleCount = msaa, | |
| .disableRasterOrdering = forceAtomicMode, | |
| .wireframe = wireframe, | |
| .fillsDisabled = disableFill, | |
| .strokesDisabled = disableStroke, | |
| .clockwiseFillOverride = clockwiseFill, | |
| }); | |
| int instances = 1; | |
| if (rivFile) | |
| { | |
| instances = (1 + horzRepeat * 2) * (1 + upRepeat + downRepeat); | |
| if (artboards.size() != instances || scenes.size() != instances) | |
| { | |
| make_scenes(instances); | |
| } | |
| else if (!paused) | |
| { | |
| float dT = 1 / 120.f; | |
| if (!unlockedLogic) | |
| { | |
| double time = glfwGetTime(); | |
| dT = time - fpsLastTimeLogic; | |
| fpsLastTimeLogic = time; | |
| } | |
| for (const auto& scene : scenes) | |
| { | |
| scene->advanceAndApply(dT); | |
| } | |
| } | |
| Mat2D m = computeAlignment(rive::Fit::contain, | |
| rive::Alignment::center, | |
| rive::AABB(0, 0, width, height), | |
| artboards.front()->bounds()); | |
| renderer->save(); | |
| m = Mat2D(scale, 0, 0, scale, translate.x, translate.y) * m; | |
| renderer->transform(m); | |
| float spacing = 200 / m.findMaxScale(); | |
| auto scene = scenes.begin(); | |
| for (int j = 0; j < upRepeat + 1 + downRepeat; ++j) | |
| { | |
| renderer->save(); | |
| renderer->transform(Mat2D::fromTranslate(-spacing * horzRepeat, | |
| (j - upRepeat) * spacing)); | |
| for (int i = 0; i < horzRepeat * 2 + 1; ++i) | |
| { | |
| (*scene++)->draw(renderer.get()); | |
| renderer->transform(Mat2D::fromTranslate(spacing, 0)); | |
| } | |
| renderer->restore(); | |
| } | |
| renderer->restore(); | |
| } | |
| else | |
| { | |
| float2 p[9]; | |
| for (int i = 0; i < 9; ++i) | |
| { | |
| p[i] = pts[i] + translate; | |
| } | |
| RawPath rawPath; | |
| rawPath.moveTo(p[0].x, p[0].y); | |
| rawPath.cubicTo(p[1].x, p[1].y, p[2].x, p[2].y, p[3].x, p[3].y); | |
| float2 c0 = simd::mix(p[3], p[4], float2(2 / 3.f)); | |
| float2 c1 = simd::mix(p[5], p[4], float2(2 / 3.f)); | |
| rawPath.cubicTo(c0.x, c0.y, c1.x, c1.y, p[5].x, p[5].y); | |
| rawPath.cubicTo(p[6].x, p[6].y, p[7].x, p[7].y, p[8].x, p[8].y); | |
| if (doClose) | |
| { | |
| rawPath.close(); | |
| } | |
| Factory* factory = fiddleContext->factory(); | |
| auto path = factory->makeRenderPath(rawPath, FillRule::clockwise); | |
| auto fillPaint = factory->makeRenderPaint(); | |
| float feather = powf(1.5f, (1400 - pts[std::size(pts) - 1].y) / 75); | |
| if (feather > 1) | |
| { | |
| fillPaint->feather(feather); | |
| } | |
| fillPaint->color(0xd0ffffff); | |
| renderer->drawPath(path.get(), fillPaint.get()); | |
| if (!disableStroke) | |
| { | |
| auto strokePaint = factory->makeRenderPaint(); | |
| strokePaint->style(RenderPaintStyle::stroke); | |
| strokePaint->color(0x8000ffff); | |
| strokePaint->thickness(strokeWidth); | |
| if (feather > 1) | |
| { | |
| strokePaint->feather(feather); | |
| } | |
| strokePaint->join(join); | |
| strokePaint->cap(cap); | |
| renderer->drawPath(path.get(), strokePaint.get()); | |
| // Draw the interactive points. | |
| auto pointPaint = factory->makeRenderPaint(); | |
| pointPaint->style(RenderPaintStyle::stroke); | |
| pointPaint->color(0xff0000ff); | |
| pointPaint->thickness(14); | |
| pointPaint->cap(StrokeCap::round); | |
| pointPaint->join(StrokeJoin::round); | |
| auto pointPath = factory->makeEmptyRenderPath(); | |
| for (int i : {1, 2, 4, 6, 7}) | |
| { | |
| float2 pt = pts[i] + translate; | |
| pointPath->moveTo(pt.x, pt.y); | |
| pointPath->close(); | |
| } | |
| renderer->drawPath(pointPath.get(), pointPaint.get()); | |
| // Draw the feather control point. | |
| pointPaint->color(0xffff0000); | |
| pointPath = factory->makeEmptyRenderPath(); | |
| float2 pt = pts[std::size(pts) - 1] + translate; | |
| pointPath->moveTo(pt.x, pt.y); | |
| renderer->drawPath(pointPath.get(), pointPaint.get()); | |
| } | |
| } | |
| fiddleContext->end(window); | |
| if (rivFile) | |
| { | |
| // Count FPS. | |
| ++fpsFrames; | |
| double time = glfwGetTime(); | |
| double fpsElapsed = time - fpsLastTime; | |
| if (fpsElapsed > 2) | |
| { | |
| int instances = (1 + horzRepeat * 2) * (1 + upRepeat + downRepeat); | |
| double fps = fpsLastTime == 0 ? 0 : fpsFrames / fpsElapsed; | |
| update_window_title(fps, instances, width, height); | |
| fpsFrames = 0; | |
| fpsLastTime = time; | |
| } | |
| } | |
| } |