| /* | |
| * Copyright 2023 Rive | |
| */ | |
| #include "rive/math/simd.hpp" | |
| #include "rive/artboard.hpp" | |
| #include "rive/file.hpp" | |
| #include "rive/layout.hpp" | |
| #include "rive/animation/linear_animation_instance.hpp" | |
| #include "rive/animation/state_machine_input_instance.hpp" | |
| #include "rive/animation/state_machine_instance.hpp" | |
| #include "rive/renderer/render_context.hpp" | |
| #include "rive/renderer/rive_renderer.hpp" | |
| #include "rive/renderer/webgpu/render_context_webgpu_impl.hpp" | |
| #include "rive/renderer/webgpu/em_js_handle.hpp" | |
| #include <cmath> | |
| #include <iterator> | |
| #include <vector> | |
| #include <filesystem> | |
| #ifdef RIVE_WEBGPU | |
| #include <webgpu/webgpu_cpp.h> | |
| #include <emscripten.h> | |
| #include <emscripten/html5_webgpu.h> | |
| #else | |
| #include <dawn/webgpu_cpp.h> | |
| #include "dawn/native/DawnNative.h" | |
| #include "dawn/dawn_proc.h" | |
| #define EMSCRIPTEN_KEEPALIVE | |
| #endif | |
| using namespace rive; | |
| using namespace rive::gpu; | |
| using PixelLocalStorageType = RenderContextWebGPUImpl::PixelLocalStorageType; | |
| static std::unique_ptr<RenderContext> s_renderContext; | |
| static rcp<RenderTargetWebGPU> s_renderTarget; | |
| static std::unique_ptr<Renderer> s_renderer; | |
| void riveInitPlayer(int w, | |
| int h, | |
| wgpu::Device gpu, | |
| wgpu::Queue queue, | |
| const gpu::PlatformFeatures& platformFeatures, | |
| PixelLocalStorageType plsType, | |
| int maxVertexStorageBlocks) | |
| { | |
| RenderContextWebGPUImpl::ContextOptions contextOptions = { | |
| .plsType = plsType, | |
| .disableStorageBuffers = | |
| maxVertexStorageBlocks < gpu::kMaxStorageBuffers, | |
| }; | |
| s_renderContext = RenderContextWebGPUImpl::MakeContext(gpu, | |
| queue, | |
| contextOptions, | |
| platformFeatures); | |
| s_renderTarget = | |
| s_renderContext->static_impl_cast<RenderContextWebGPUImpl>() | |
| ->makeRenderTarget(wgpu::TextureFormat::BGRA8Unorm, w, h); | |
| s_renderer = std::make_unique<RiveRenderer>(s_renderContext.get()); | |
| } | |
| #ifdef RIVE_WEBGPU | |
| EmJsHandle s_deviceHandle; | |
| EmJsHandle s_queueHandle; | |
| EmJsHandle s_textureViewHandle; | |
| extern "C" | |
| { | |
| void EMSCRIPTEN_KEEPALIVE RiveInitialize(int deviceID, | |
| int queueID, | |
| int canvasWidth, | |
| int canvasHeight, | |
| bool invertedY, | |
| int pixelLocalStorageType, | |
| int maxVertexStorageBlocks) | |
| { | |
| s_deviceHandle = EmJsHandle(deviceID); | |
| s_queueHandle = EmJsHandle(queueID); | |
| gpu::PlatformFeatures platformFeatures; | |
| if (invertedY) | |
| { | |
| platformFeatures.uninvertOnScreenY = true; | |
| } | |
| riveInitPlayer( | |
| canvasWidth, | |
| canvasHeight, | |
| wgpu::Device::Acquire( | |
| emscripten_webgpu_import_device(s_deviceHandle.get())), | |
| emscripten_webgpu_import_queue(s_queueHandle.get()), | |
| platformFeatures, | |
| static_cast<PixelLocalStorageType>(pixelLocalStorageType), | |
| maxVertexStorageBlocks); | |
| } | |
| intptr_t EMSCRIPTEN_KEEPALIVE RiveBeginRendering(int textureViewID, | |
| int loadAction, | |
| ColorInt clearColor) | |
| { | |
| s_textureViewHandle = EmJsHandle(textureViewID); | |
| auto targetTextureView = wgpu::TextureView::Acquire( | |
| emscripten_webgpu_import_texture_view(s_textureViewHandle.get())); | |
| s_renderTarget->setTargetTextureView(targetTextureView); | |
| s_renderContext->beginFrame({ | |
| .renderTargetWidth = s_renderTarget->width(), | |
| .renderTargetHeight = s_renderTarget->height(), | |
| .loadAction = static_cast<gpu::LoadAction>(loadAction), | |
| .clearColor = clearColor, | |
| }); | |
| s_renderer->save(); | |
| return reinterpret_cast<intptr_t>(s_renderer.get()); | |
| } | |
| void EMSCRIPTEN_KEEPALIVE RiveFlushRendering() | |
| { | |
| s_renderer->restore(); | |
| s_renderContext->flush({.renderTarget = s_renderTarget.get()}); | |
| s_textureViewHandle = EmJsHandle(); | |
| } | |
| intptr_t EMSCRIPTEN_KEEPALIVE RiveLoadFile(intptr_t wasmBytesPtr, | |
| size_t length) | |
| { | |
| const uint8_t* bytes = reinterpret_cast<const uint8_t*>(wasmBytesPtr); | |
| std::unique_ptr<File> file = | |
| File::import({bytes, length}, s_renderContext.get()); | |
| return reinterpret_cast<intptr_t>(file.release()); | |
| } | |
| intptr_t EMSCRIPTEN_KEEPALIVE File_artboardNamed(intptr_t nativePtr, | |
| const char* name) | |
| { | |
| auto file = reinterpret_cast<File*>(nativePtr); | |
| std::unique_ptr<ArtboardInstance> artboard = file->artboardNamed(name); | |
| return reinterpret_cast<intptr_t>(artboard.release()); | |
| } | |
| intptr_t EMSCRIPTEN_KEEPALIVE File_artboardDefault(intptr_t nativePtr) | |
| { | |
| auto file = reinterpret_cast<File*>(nativePtr); | |
| std::unique_ptr<ArtboardInstance> artboard = file->artboardDefault(); | |
| return reinterpret_cast<intptr_t>(artboard.release()); | |
| } | |
| void EMSCRIPTEN_KEEPALIVE File_destroy(intptr_t nativePtr) | |
| { | |
| auto file = reinterpret_cast<File*>(nativePtr); | |
| delete file; | |
| } | |
| int EMSCRIPTEN_KEEPALIVE ArtboardInstance_width(intptr_t nativePtr) | |
| { | |
| auto artboard = reinterpret_cast<ArtboardInstance*>(nativePtr); | |
| return artboard->bounds().width(); | |
| } | |
| int EMSCRIPTEN_KEEPALIVE ArtboardInstance_height(intptr_t nativePtr) | |
| { | |
| auto artboard = reinterpret_cast<ArtboardInstance*>(nativePtr); | |
| return artboard->bounds().height(); | |
| } | |
| intptr_t EMSCRIPTEN_KEEPALIVE | |
| ArtboardInstance_stateMachineNamed(intptr_t nativePtr, const char* name) | |
| { | |
| auto artboard = reinterpret_cast<ArtboardInstance*>(nativePtr); | |
| std::unique_ptr<StateMachineInstance> stateMachine = | |
| artboard->stateMachineNamed(name); | |
| return reinterpret_cast<intptr_t>(stateMachine.release()); | |
| } | |
| intptr_t EMSCRIPTEN_KEEPALIVE | |
| ArtboardInstance_animationNamed(intptr_t nativePtr, const char* name) | |
| { | |
| auto artboard = reinterpret_cast<ArtboardInstance*>(nativePtr); | |
| std::unique_ptr<LinearAnimationInstance> animation = | |
| artboard->animationNamed(name); | |
| return reinterpret_cast<intptr_t>(animation.release()); | |
| } | |
| intptr_t EMSCRIPTEN_KEEPALIVE | |
| ArtboardInstance_defaultStateMachine(intptr_t nativePtr) | |
| { | |
| auto artboard = reinterpret_cast<ArtboardInstance*>(nativePtr); | |
| std::unique_ptr<StateMachineInstance> stateMachine = | |
| artboard->defaultStateMachine(); | |
| return reinterpret_cast<intptr_t>(stateMachine.release()); | |
| } | |
| void EMSCRIPTEN_KEEPALIVE ArtboardInstance_align(intptr_t nativePtr, | |
| intptr_t rendererNativePtr, | |
| float frameLeft, | |
| float frameTop, | |
| float frameRight, | |
| float frameBot, | |
| int fitValue, | |
| float alignmentX, | |
| float alignmentY) | |
| { | |
| auto artboard = reinterpret_cast<ArtboardInstance*>(nativePtr); | |
| auto renderer = reinterpret_cast<RiveRenderer*>(rendererNativePtr); | |
| auto fit = static_cast<Fit>(fitValue); | |
| Alignment alignment = {alignmentX, alignmentY}; | |
| AABB frame = {frameLeft, frameTop, frameRight, frameBot}; | |
| renderer->align(fit, alignment, frame, artboard->bounds()); | |
| } | |
| void EMSCRIPTEN_KEEPALIVE ArtboardInstance_destroy(intptr_t nativePtr) | |
| { | |
| auto artboard = reinterpret_cast<ArtboardInstance*>(nativePtr); | |
| delete artboard; | |
| } | |
| void EMSCRIPTEN_KEEPALIVE | |
| StateMachineInstance_setBool(intptr_t nativePtr, | |
| const char* inputName, | |
| int value) | |
| { | |
| auto stateMachine = reinterpret_cast<StateMachineInstance*>(nativePtr); | |
| if (SMIBool* input = stateMachine->getBool(inputName)) | |
| { | |
| input->value(static_cast<bool>(value)); | |
| } | |
| } | |
| void EMSCRIPTEN_KEEPALIVE | |
| StateMachineInstance_setNumber(intptr_t nativePtr, | |
| const char* inputName, | |
| float value) | |
| { | |
| auto stateMachine = reinterpret_cast<StateMachineInstance*>(nativePtr); | |
| if (SMINumber* input = stateMachine->getNumber(inputName)) | |
| { | |
| input->value(value); | |
| } | |
| } | |
| void EMSCRIPTEN_KEEPALIVE | |
| StateMachineInstance_fireTrigger(intptr_t nativePtr, const char* inputName) | |
| { | |
| auto stateMachine = reinterpret_cast<StateMachineInstance*>(nativePtr); | |
| if (SMITrigger* input = stateMachine->getTrigger(inputName)) | |
| { | |
| input->fire(); | |
| } | |
| } | |
| void EMSCRIPTEN_KEEPALIVE | |
| StateMachineInstance_pointerDown(intptr_t nativePtr, float x, float y) | |
| { | |
| auto stateMachine = reinterpret_cast<StateMachineInstance*>(nativePtr); | |
| stateMachine->pointerDown({x, y}); | |
| } | |
| void EMSCRIPTEN_KEEPALIVE | |
| StateMachineInstance_pointerMove(intptr_t nativePtr, float x, float y) | |
| { | |
| auto stateMachine = reinterpret_cast<StateMachineInstance*>(nativePtr); | |
| stateMachine->pointerMove({x, y}); | |
| } | |
| void EMSCRIPTEN_KEEPALIVE StateMachineInstance_pointerUp(intptr_t nativePtr, | |
| float x, | |
| float y) | |
| { | |
| auto stateMachine = reinterpret_cast<StateMachineInstance*>(nativePtr); | |
| stateMachine->pointerUp({x, y}); | |
| } | |
| void EMSCRIPTEN_KEEPALIVE | |
| StateMachineInstance_advanceAndApply(intptr_t nativePtr, double elapsed) | |
| { | |
| auto stateMachine = reinterpret_cast<StateMachineInstance*>(nativePtr); | |
| stateMachine->advanceAndApply(elapsed); | |
| } | |
| void EMSCRIPTEN_KEEPALIVE | |
| StateMachineInstance_draw(intptr_t nativePtr, intptr_t rendererNativePtr) | |
| { | |
| auto stateMachine = reinterpret_cast<StateMachineInstance*>(nativePtr); | |
| auto renderer = reinterpret_cast<RiveRenderer*>(rendererNativePtr); | |
| stateMachine->draw(renderer); | |
| } | |
| void EMSCRIPTEN_KEEPALIVE StateMachineInstance_destroy(intptr_t nativePtr) | |
| { | |
| auto stateMachine = reinterpret_cast<StateMachineInstance*>(nativePtr); | |
| delete stateMachine; | |
| } | |
| void EMSCRIPTEN_KEEPALIVE | |
| LinearAnimationInstance_advanceAndApply(intptr_t nativePtr, double elapsed) | |
| { | |
| auto animation = reinterpret_cast<LinearAnimationInstance*>(nativePtr); | |
| animation->advanceAndApply(elapsed); | |
| } | |
| void EMSCRIPTEN_KEEPALIVE | |
| LinearAnimationInstance_draw(intptr_t nativePtr, intptr_t rendererNativePtr) | |
| { | |
| auto animation = reinterpret_cast<LinearAnimationInstance*>(nativePtr); | |
| auto renderer = reinterpret_cast<RiveRenderer*>(rendererNativePtr); | |
| animation->draw(renderer); | |
| } | |
| void EMSCRIPTEN_KEEPALIVE | |
| LinearAnimationInstance_destroy(intptr_t nativePtr) | |
| { | |
| auto animation = reinterpret_cast<LinearAnimationInstance*>(nativePtr); | |
| delete animation; | |
| } | |
| void EMSCRIPTEN_KEEPALIVE Renderer_save(intptr_t nativePtr) | |
| { | |
| auto renderer = reinterpret_cast<RiveRenderer*>(nativePtr); | |
| renderer->save(); | |
| } | |
| void EMSCRIPTEN_KEEPALIVE Renderer_restore(intptr_t nativePtr) | |
| { | |
| auto renderer = reinterpret_cast<RiveRenderer*>(nativePtr); | |
| renderer->restore(); | |
| } | |
| void EMSCRIPTEN_KEEPALIVE Renderer_translate(intptr_t nativePtr, | |
| float x, | |
| float y) | |
| { | |
| auto renderer = reinterpret_cast<RiveRenderer*>(nativePtr); | |
| renderer->translate(x, y); | |
| } | |
| void EMSCRIPTEN_KEEPALIVE Renderer_transform(intptr_t nativePtr, | |
| float xx, | |
| float xy, | |
| float yx, | |
| float yy, | |
| float tx, | |
| float ty) | |
| { | |
| auto renderer = reinterpret_cast<RiveRenderer*>(nativePtr); | |
| Mat2D matrix(xx, xy, yx, yy, tx, ty); | |
| renderer->transform(matrix); | |
| } | |
| } | |
| #endif | |
| #ifdef RIVE_DAWN | |
| #include <GLFW/glfw3.h> | |
| #ifndef __APPLE__ | |
| #define GLFW_EXPOSE_NATIVE_WIN32 | |
| #include <GLFW/glfw3native.h> | |
| #endif | |
| #include <fstream> | |
| static void glfw_error_callback(int code, const char* message) | |
| { | |
| printf("GLFW error: %i - %s\n", code, message); | |
| } | |
| static void print_device_error(WGPUErrorType errorType, | |
| const char* message, | |
| void*) | |
| { | |
| const char* errorTypeName = ""; | |
| switch (errorType) | |
| { | |
| case WGPUErrorType_Validation: | |
| errorTypeName = "Validation"; | |
| break; | |
| case WGPUErrorType_OutOfMemory: | |
| errorTypeName = "Out of memory"; | |
| break; | |
| case WGPUErrorType_Unknown: | |
| errorTypeName = "Unknown"; | |
| break; | |
| case WGPUErrorType_DeviceLost: | |
| errorTypeName = "Device lost"; | |
| break; | |
| default: | |
| RIVE_UNREACHABLE(); | |
| return; | |
| } | |
| printf("%s error: %s\n", errorTypeName, message); | |
| } | |
| static void device_lost_callback(WGPUDeviceLostReason reason, | |
| const char* message, | |
| void*) | |
| { | |
| printf("device lost: %s\n", message); | |
| } | |
| static void device_log_callback(WGPULoggingType type, | |
| const char* message, | |
| void*) | |
| { | |
| printf("Device log %s\n", message); | |
| } | |
| static GLFWwindow* s_window = nullptr; | |
| static WGPUDevice s_backendDevice = {}; | |
| static wgpu::SwapChain s_swapchain = {}; | |
| static std::unique_ptr<dawn::native::Instance> s_instance; | |
| #ifdef __APPLE__ | |
| extern float GetDawnWindowBackingScaleFactor(GLFWwindow*, bool retina); | |
| extern std::unique_ptr<wgpu::ChainedStruct> | |
| SetupDawnWindowAndGetSurfaceDescriptor(GLFWwindow*, bool retina); | |
| #else | |
| #define GLFW_EXPOSE_NATIVE_WIN32 | |
| #include <GLFW/glfw3.h> | |
| #include <GLFW/glfw3native.h> | |
| static std::unique_ptr<wgpu::ChainedStruct> | |
| SetupDawnWindowAndGetSurfaceDescriptor(GLFWwindow* window, bool retina) | |
| { | |
| std::unique_ptr<wgpu::SurfaceDescriptorFromWindowsHWND> desc = | |
| std::make_unique<wgpu::SurfaceDescriptorFromWindowsHWND>(); | |
| desc->hwnd = glfwGetWin32Window(window); | |
| desc->hinstance = GetModuleHandle(nullptr); | |
| return std::move(desc); | |
| } | |
| #endif | |
| #endif | |
| #ifdef RIVE_DAWN | |
| 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); | |
| glfwSetErrorCallback(glfw_error_callback); | |
| if (!glfwInit()) | |
| { | |
| fprintf(stderr, "Failed to initialize glfw.\n"); | |
| return 1; | |
| } | |
| glfwWindowHint(GLFW_SAMPLES, 0); | |
| glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); | |
| glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); | |
| glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW_TRUE); | |
| glfwWindowHint(GLFW_FOCUS_ON_SHOW, GLFW_TRUE); | |
| glfwWindowHint(GLFW_FLOATING, GLFW_TRUE); | |
| s_window = glfwCreateWindow(1600, 1600, "Rive Renderer", nullptr, nullptr); | |
| if (!s_window) | |
| { | |
| glfwTerminate(); | |
| fprintf(stderr, "Failed to create window.\n"); | |
| return -1; | |
| } | |
| glfwSetWindowTitle(s_window, "Rive Renderer"); | |
| WGPUInstanceDescriptor instanceDescriptor{}; | |
| instanceDescriptor.features.timedWaitAnyEnable = true; | |
| s_instance = std::make_unique<dawn::native::Instance>(&instanceDescriptor); | |
| wgpu::RequestAdapterOptions adapterOptions = {}; | |
| // Get an adapter for the backend to use, and create the device. | |
| auto adapters = s_instance->EnumerateAdapters(&adapterOptions); | |
| wgpu::DawnAdapterPropertiesPowerPreference power_props{}; | |
| wgpu::AdapterProperties adapterProperties{}; | |
| adapterProperties.nextInChain = &power_props; | |
| // Find the first adapter which satisfies the adapterType requirement. | |
| auto isAdapterType = [&adapterProperties](const auto& adapter) -> bool { | |
| adapter.GetProperties(&adapterProperties); | |
| return adapterProperties.adapterType == wgpu::AdapterType::DiscreteGPU; | |
| }; | |
| auto preferredAdapter = | |
| std::find_if(adapters.begin(), adapters.end(), isAdapterType); | |
| if (preferredAdapter == adapters.end()) | |
| { | |
| fprintf( | |
| stderr, | |
| "Failed to find an adapter! Please try another adapter type.\n"); | |
| abort(); | |
| } | |
| std::vector<const char*> enableToggleNames = { | |
| "allow_unsafe_apis", | |
| "turn_off_vsync", | |
| // "skip_validation", | |
| }; | |
| std::vector<const char*> disabledToggleNames; | |
| WGPUDawnTogglesDescriptor toggles = { | |
| .chain = | |
| { | |
| .next = nullptr, | |
| .sType = WGPUSType_DawnTogglesDescriptor, | |
| }, | |
| .enabledToggleCount = enableToggleNames.size(), | |
| .enabledToggles = enableToggleNames.data(), | |
| .disabledToggleCount = disabledToggleNames.size(), | |
| .disabledToggles = disabledToggleNames.data(), | |
| }; | |
| std::vector<WGPUFeatureName> requiredFeatures = {}; | |
| WGPUDeviceDescriptor deviceDesc = { | |
| .nextInChain = reinterpret_cast<WGPUChainedStruct*>(&toggles), | |
| .requiredFeatureCount = requiredFeatures.size(), | |
| .requiredFeatures = requiredFeatures.data(), | |
| }; | |
| s_backendDevice = preferredAdapter->CreateDevice(&deviceDesc); | |
| DawnProcTable backendProcs = dawn::native::GetProcs(); | |
| dawnProcSetProcs(&backendProcs); | |
| backendProcs.deviceSetUncapturedErrorCallback(s_backendDevice, | |
| print_device_error, | |
| nullptr); | |
| backendProcs.deviceSetDeviceLostCallback(s_backendDevice, | |
| device_lost_callback, | |
| nullptr); | |
| backendProcs.deviceSetLoggingCallback(s_backendDevice, | |
| device_log_callback, | |
| nullptr); | |
| wgpu::Device device = wgpu::Device::Acquire(s_backendDevice); | |
| int w, h; | |
| glfwGetFramebufferSize(s_window, &w, &h); | |
| // Create the swapchain | |
| auto surfaceChainedDesc = | |
| SetupDawnWindowAndGetSurfaceDescriptor(s_window, true); | |
| WGPUSurfaceDescriptor surfaceDesc = { | |
| .nextInChain = | |
| reinterpret_cast<WGPUChainedStruct*>(surfaceChainedDesc.get()), | |
| }; | |
| WGPUSurface surface = | |
| backendProcs.instanceCreateSurface(s_instance->Get(), &surfaceDesc); | |
| WGPUSwapChainDescriptor swapChainDesc = { | |
| .usage = WGPUTextureUsage_RenderAttachment, | |
| .format = WGPUTextureFormat_BGRA8Unorm, | |
| .width = static_cast<uint32_t>(w), | |
| .height = static_cast<uint32_t>(h), | |
| .presentMode = WGPUPresentMode_Immediate, // No vsync. | |
| }; | |
| WGPUSwapChain backendSwapChain = | |
| backendProcs.deviceCreateSwapChain(s_backendDevice, | |
| surface, | |
| &swapChainDesc); | |
| s_swapchain = wgpu::SwapChain::Acquire(backendSwapChain); | |
| riveInitPlayer(w, | |
| h, | |
| device.Get(), | |
| device.GetQueue(), | |
| PlatformFeatures{}, | |
| PixelLocalStorageType::none, | |
| 8); | |
| std::ifstream rivStream("../../../gold/rivs/Santa_Claus.riv", | |
| std::ios::binary); | |
| std::vector<uint8_t> rivBytes(std::istreambuf_iterator<char>(rivStream), | |
| {}); | |
| std::unique_ptr<File> rivFile = | |
| File::import(rivBytes, s_renderContext.get()); | |
| std::unique_ptr<ArtboardInstance> artboard = rivFile->artboardDefault(); | |
| std::unique_ptr<Scene> scene = artboard->defaultScene(); | |
| scene->advanceAndApply(0); | |
| double lastTimestamp = 0; | |
| while (!glfwWindowShouldClose(s_window)) | |
| { | |
| double timestamp = glfwGetTime(); | |
| s_renderTarget->setTargetTextureView( | |
| s_swapchain.GetCurrentTextureView()); | |
| s_renderContext->beginFrame({ | |
| .renderTargetWidth = s_renderTarget->width(), | |
| .renderTargetHeight = s_renderTarget->height(), | |
| .clearColor = 0xff8030ff, | |
| }); | |
| s_renderer->save(); | |
| s_renderer->transform(computeAlignment(rive::Fit::contain, | |
| rive::Alignment::center, | |
| rive::AABB(0, 0, w, h), | |
| artboard->bounds())); | |
| scene->draw(s_renderer.get()); | |
| s_renderer->restore(); | |
| s_renderContext->flush({.renderTarget = s_renderTarget.get()}); | |
| s_swapchain.Present(); | |
| device.Tick(); | |
| glfwPollEvents(); | |
| if (lastTimestamp != 0) | |
| { | |
| scene->advanceAndApply(timestamp - lastTimestamp); | |
| } | |
| lastTimestamp = timestamp; | |
| } | |
| glfwTerminate(); | |
| return 0; | |
| } | |
| #endif |