diff --git a/NativeScript/runtime/AnimationFrame.hpp b/NativeScript/runtime/AnimationFrame.hpp new file mode 100644 index 00000000..80471382 --- /dev/null +++ b/NativeScript/runtime/AnimationFrame.hpp @@ -0,0 +1,31 @@ +// +// AnimationFrame.hpp +// NativeScript +// + +#ifndef AnimationFrame_hpp +#define AnimationFrame_hpp + +#include "Common.h" + +namespace tns { + +class AnimationFrame { + public: + static void Init(v8::Isolate* isolate, + v8::Local globalTemplate); + + private: + static void RequestAnimationFrame( + const v8::FunctionCallbackInfo& info); + static void CancelAnimationFrame( + const v8::FunctionCallbackInfo& info); + static void PostFrameCallback( + const v8::FunctionCallbackInfo& info); + static void RemoveFrameCallback( + const v8::FunctionCallbackInfo& info); +}; + +} // namespace tns + +#endif /* AnimationFrame_hpp */ diff --git a/NativeScript/runtime/AnimationFrame.mm b/NativeScript/runtime/AnimationFrame.mm new file mode 100644 index 00000000..09994f39 --- /dev/null +++ b/NativeScript/runtime/AnimationFrame.mm @@ -0,0 +1,360 @@ +// +// AnimationFrame.mm +// NativeScript +// + +#include "AnimationFrame.hpp" + +#import +#import + +#include +#include + +#include "Caches.h" +#include "Helpers.h" +#include "IsolateWrapper.h" +#include "ModuleBinding.hpp" +#include "Runtime.h" +#include "robin_hood.h" + +/* + * Frame callbacks ride one CADisplayLink per isolate, created paused during + * isolate initialization on the isolate's home thread and attached to that + * thread's runloop in the common modes (so ticks keep arriving during + * scroll/tracking, matching the EventLoop's timers). Posting unpauses the + * link; an empty registry pauses it again so an idle isolate never wakes per + * frame. All access — post, remove, dispatch, teardown — happens on the home + * thread, so the registry needs no locking. + * + * Two front-ends share the registry, matching the Android runtime's contract: + * - requestAnimationFrame(fn) / cancelAnimationFrame(handle): the standard + * surface. Every request posts its own anonymous one-shot entry addressed + * only by the returned handle (no dedup by function), and fn receives a + * single DOMHighResTimeStamp on the isolate's performance timeline. + * cancelAnimationFrame may only touch entries carrying the raf flag. + * - __postFrameCallback(fn[, delayMillis]) / __removeFrameCallback(fn): the + * compatibility surface predating requestAnimationFrame. One-shot, deduped + * by function identity (the entry id is stamped on the function as a + * private value): re-posting a pending callback is a no-op, delay included. + * fn(frameTimeNanos, performanceMillis) gets the raw frame time as well. A + * delayed entry fires on the first frame after its delay elapses. + * + * Each tick dispatches only the entries scheduled before it (the order list + * is swapped out first), so a callback that re-posts runs again next frame, + * never in the same batch. Every callback in a batch observes the same + * timestamps. CADisplayLink.timestamp shares mach_absolute_time as its base + * with the V8 platform clock (and CACurrentMediaTime), so it maps onto the + * performance timeline via Runtime::TimeOriginMonotonicSeconds() — never + * re-sample the clock at dispatch, the vsync instant is the contract. + */ + +using namespace v8; + +namespace tns { +class AnimationFrameState; +} + +@interface TNSAnimationFrameTarget : NSObject { + @public + tns::AnimationFrameState* state_; +} +- (void)onFrame:(CADisplayLink*)link; +@end + +namespace tns { + +static constexpr const char* kFrameCallbackIdKey = "_postFrameCallbackId"; + +struct FrameEntry { + v8::Persistent callback; + // scheduled == false only while the entry's callback is being invoked; a + // re-post inside the callback flips it back and the entry survives the + // post-call retirement check + bool scheduled = false; + bool raf = false; + // earliest frame timestamp (CADisplayLink.timestamp base) this entry may + // fire at; 0 means the next frame + double notBeforeSeconds = 0; +}; + +class AnimationFrameState { + public: + explicit AnimationFrameState(v8::Isolate* isolate) : isolate_(isolate), wrapper_(isolate) { + @autoreleasepool { + target_ = [[TNSAnimationFrameTarget alloc] init]; + target_->state_ = this; + displayLink_ = [[CADisplayLink displayLinkWithTarget:target_ + selector:@selector(onFrame:)] retain]; + displayLink_.paused = YES; + [displayLink_ addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; + } + } + + ~AnimationFrameState() { + if (displayLink_ != nil) { + [displayLink_ invalidate]; + [displayLink_ release]; + displayLink_ = nil; + } + if (target_ != nil) { + target_->state_ = nullptr; + [target_ release]; + target_ = nil; + } + // Caches teardown runs before Isolate::Dispose, which is what makes + // resetting the persistents here legal; guard anyway like TimerTask does + if (wrapper_.IsValid()) { + for (auto& entry : entries_) { + entry.second->callback.Reset(); + } + } + entries_.clear(); + order_.clear(); + } + + void Post(Isolate* isolate, const Local& func, double delayMillis) { + double notBefore = delayMillis > 0 ? CACurrentMediaTime() + delayMillis / 1000.0 : 0; + Local existingId = + tns::GetPrivateValue(func, tns::ToV8String(isolate, kFrameCallbackIdKey)); + if (!existingId.IsEmpty() && existingId->IsNumber()) { + // ids are never reused, so a hit is always this function's own entry; + // a miss means the entry already fired or was removed + auto it = entries_.find((uint64_t)existingId.As()->Value()); + if (it != entries_.end()) { + // an already-pending entry keeps its slot and its delay + if (!it->second->scheduled) { + it->second->scheduled = true; + it->second->notBeforeSeconds = notBefore; + order_.push_back(it->first); + } + EnsureRunning(); + return; + } + } + uint64_t id = ++nextId_; + auto entry = std::make_unique(); + entry->callback.Reset(isolate, func); +#ifdef DEBUG + entry->callback.AnnotateStrongRetainer("frame_callback"); +#endif + entry->scheduled = true; + entry->notBeforeSeconds = notBefore; + entries_.emplace(id, std::move(entry)); + order_.push_back(id); + tns::SetPrivateValue(func, tns::ToV8String(isolate, kFrameCallbackIdKey), + v8::Number::New(isolate, (double)id)); + EnsureRunning(); + } + + void Remove(Isolate* isolate, const Local& func) { + Local existingId = + tns::GetPrivateValue(func, tns::ToV8String(isolate, kFrameCallbackIdKey)); + if (existingId.IsEmpty() || !existingId->IsNumber()) { + return; + } + Erase((uint64_t)existingId.As()->Value()); + } + + uint64_t Request(Isolate* isolate, const Local& func) { + uint64_t id = ++nextId_; + auto entry = std::make_unique(); + entry->callback.Reset(isolate, func); +#ifdef DEBUG + entry->callback.AnnotateStrongRetainer("animation_frame"); +#endif + entry->scheduled = true; + entry->raf = true; + entries_.emplace(id, std::move(entry)); + order_.push_back(id); + EnsureRunning(); + return id; + } + + void Cancel(uint64_t id) { + auto it = entries_.find(id); + // handles only name requestAnimationFrame entries; a __postFrameCallback + // entry that happens to share the counter must stay untouchable from here + if (it == entries_.end() || !it->second->raf) { + return; + } + it->second->callback.Reset(); + entries_.erase(it); + PauseIfIdle(); + } + + void Erase(uint64_t id) { + auto it = entries_.find(id); + if (it == entries_.end()) { + return; + } + // the id may still sit in a pending batch; dispatch tolerates the miss + it->second->callback.Reset(); + entries_.erase(it); + PauseIfIdle(); + } + + void FireFrame(double timestampSeconds) { + Isolate* isolate = isolate_; + if (isolate == nullptr || !wrapper_.IsValid() || isolate->IsDead()) { + return; + } + Runtime* runtime = Runtime::GetRuntime(isolate); + if (runtime == nullptr) { + return; + } + double frameTimeNanos = timestampSeconds * 1e9; + double performanceMillis = (timestampSeconds - runtime->TimeOriginMonotonicSeconds()) * 1000.0; + + v8::Locker locker(isolate); + v8::Isolate::Scope isolateScope(isolate); + v8::HandleScope handleScope(isolate); + + std::vector batch = std::move(order_); + order_.clear(); + for (uint64_t id : batch) { + auto it = entries_.find(id); + if (it == entries_.end() || !it->second->scheduled) { + continue; + } + FrameEntry* entry = it->second.get(); + if (entry->notBeforeSeconds > timestampSeconds) { + // delay not yet elapsed: stays scheduled, moves to the next batch + order_.push_back(id); + continue; + } + entry->scheduled = false; + Local cb = entry->callback.Get(isolate); + Local context = cb->GetCreationContextChecked(v8::Isolate::GetCurrent()); + Context::Scope contextScope(context); + if (entry->raf) { + Local argv[] = {v8::Number::New(isolate, performanceMillis)}; + (void)cb->Call(context, context->Global(), 1, argv); + } else { + Local argv[] = {v8::Number::New(isolate, frameTimeNanos), + v8::Number::New(isolate, performanceMillis)}; + (void)cb->Call(context, context->Global(), 2, argv); + } + // re-resolve: the callback may have removed entries (this one included) + // or re-posted itself + auto post = entries_.find(id); + if (post != entries_.end() && !post->second->scheduled) { + post->second->callback.Reset(); + entries_.erase(post); + } + } + PauseIfIdle(); + } + + private: + void EnsureRunning() { + if (displayLink_ != nil && displayLink_.paused) { + displayLink_.paused = NO; + } + } + + void PauseIfIdle() { + if (displayLink_ != nil && entries_.empty()) { + displayLink_.paused = YES; + } + } + + v8::Isolate* isolate_; + IsolateWrapper wrapper_; + uint64_t nextId_ = 0; + robin_hood::unordered_map> entries_; + // entry ids in post order; the upcoming tick's batch. An id appears at most + // once: it is pushed only on the not-scheduled -> scheduled transition (or + // carried over while its delay runs down) + std::vector order_; + CADisplayLink* displayLink_ = nil; + TNSAnimationFrameTarget* target_ = nil; +}; + +static AnimationFrameState* StateFromInfo(const FunctionCallbackInfo& info) { + auto extData = info.Data().As(); + return reinterpret_cast(extData->Value(v8::kExternalPointerTypeTagDefault)); +} + +static bool GetFunctionArg(const FunctionCallbackInfo& info, const char* message, + Local& func) { + Isolate* isolate = info.GetIsolate(); + if (info.Length() < 1 || !info[0]->IsFunction()) { + isolate->ThrowException(Exception::TypeError(tns::ToV8String(isolate, message))); + return false; + } + func = info[0].As(); + return true; +} + +void AnimationFrame::Init(Isolate* isolate, Local globalTemplate) { + auto state = new AnimationFrameState(isolate); + Caches::Get(isolate)->registerCacheBoundObject(state); + Local data = v8::External::New(isolate, state, v8::kExternalPointerTypeTagDefault); + tns::SetMethod(isolate, globalTemplate, "requestAnimationFrame", + AnimationFrame::RequestAnimationFrame, data); + tns::SetMethod(isolate, globalTemplate, "cancelAnimationFrame", + AnimationFrame::CancelAnimationFrame, data); + tns::SetMethod(isolate, globalTemplate, "__postFrameCallback", AnimationFrame::PostFrameCallback, + data); + tns::SetMethod(isolate, globalTemplate, "__removeFrameCallback", + AnimationFrame::RemoveFrameCallback, data); +} + +void AnimationFrame::RequestAnimationFrame(const FunctionCallbackInfo& info) { + Local func; + if (!GetFunctionArg(info, "Animation frame callback argument is not a function", func)) { + return; + } + uint64_t id = StateFromInfo(info)->Request(info.GetIsolate(), func); + info.GetReturnValue().Set((double)id); +} + +void AnimationFrame::CancelAnimationFrame(const FunctionCallbackInfo& info) { + // per spec an unknown or malformed handle is a silent no-op + if (info.Length() < 1 || !info[0]->IsNumber()) { + return; + } + double id = info[0].As()->Value(); + if (id <= 0 || !std::isfinite(id)) { + return; + } + StateFromInfo(info)->Cancel((uint64_t)id); +} + +void AnimationFrame::PostFrameCallback(const FunctionCallbackInfo& info) { + Local func; + if (!GetFunctionArg(info, "Frame callback argument is not a function", func)) { + return; + } + double delayMillis = 0; + if (info.Length() >= 2 && info[1]->IsNumber()) { + delayMillis = info[1].As()->Value(); + if (!std::isfinite(delayMillis)) { + delayMillis = 0; + } + } + StateFromInfo(info)->Post(info.GetIsolate(), func, delayMillis); +} + +void AnimationFrame::RemoveFrameCallback(const FunctionCallbackInfo& info) { + Local func; + if (!GetFunctionArg(info, "Frame callback argument is not a function", func)) { + return; + } + StateFromInfo(info)->Remove(info.GetIsolate(), func); +} + +} // namespace tns + +@implementation TNSAnimationFrameTarget + +- (void)onFrame:(CADisplayLink*)link { + if (state_ != nullptr) { + state_->FireFrame(link.timestamp); + } +} + +@end + +NODE_BINDING_PER_ISOLATE_INIT_OBJ(animationframe, tns::AnimationFrame::Init) diff --git a/NativeScript/runtime/ModuleBinding.hpp b/NativeScript/runtime/ModuleBinding.hpp index dd259abb..fff9ab9b 100644 --- a/NativeScript/runtime/ModuleBinding.hpp +++ b/NativeScript/runtime/ModuleBinding.hpp @@ -57,11 +57,12 @@ namespace tns { // V(worker) #define NODE_BINDINGS_WITH_PER_ISOLATE_INIT(V) \ -V(worker) \ -V(timers) \ -V(url) \ -V(urlsearchparams) \ -V(urlpattern) + V(worker) \ + V(timers) \ + V(animationframe) \ + V(url) \ + V(urlsearchparams) \ + V(urlpattern) enum { NM_F_BUILTIN = 1 << 0, // Unused. diff --git a/TestRunner/app/tests/AnimationFrameTests.js b/TestRunner/app/tests/AnimationFrameTests.js new file mode 100644 index 00000000..b6719cd8 --- /dev/null +++ b/TestRunner/app/tests/AnimationFrameTests.js @@ -0,0 +1,272 @@ +// Contract port of the Android runtime's testPostFrameCallback.js: the two +// frame-callback surfaces must be indistinguishable across platforms from JS. +describe("test PostFrameCallback", function () { + const defaultWaitTime = 300; + it("__postFrameCallback exists", () => { + expect(global.__postFrameCallback).toBeDefined(); + }); + + it("__removeFrameCallback exists", () => { + expect(global.__removeFrameCallback).toBeDefined(); + }); + + it("should throw when providing wrong arguments", () => { + expect(() => global.__postFrameCallback(null)).toThrow(); + expect(() => global.__removeFrameCallback(null)).toThrow(); + expect(() => global.__postFrameCallback("")).toThrow(); + expect(() => global.__removeFrameCallback("")).toThrow(); + expect(() => global.__postFrameCallback()).toThrow(); + expect(() => global.__removeFrameCallback()).toThrow(); + }); + + it("should call the callback once", (done) => { + let callCount = 0; + const callback = () => { + callCount++; + }; + global.__postFrameCallback(callback); + setTimeout(() => { + expect(callCount).toBe(1); + done(); + }, defaultWaitTime); + }); + + it("should pass the frame time and a performance-timeline timestamp", (done) => { + global.__postFrameCallback(function (frameTimeNanos, performanceMillis) { + expect(arguments.length).toBe(2); + expect(typeof frameTimeNanos).toBe("number"); + expect(typeof performanceMillis).toBe("number"); + expect(frameTimeNanos).toBeGreaterThan(0); + const now = performance.now(); + expect(performanceMillis).toBeGreaterThan(0); + expect(performanceMillis).not.toBeGreaterThan(now); + expect(now - performanceMillis).toBeLessThan(250); + done(); + }); + }); + + it("should call the callback once even if scheduled multiple times", (done) => { + let callCount = 0; + const callback = () => { + callCount++; + }; + global.__postFrameCallback(callback); + global.__postFrameCallback(callback); + setTimeout(() => { + expect(callCount).toBe(1); + done(); + }, defaultWaitTime); + }); + + it("should not trigger the callback if it was canceled", (done) => { + let callCount = 0; + const callback = () => { + callCount++; + }; + global.__postFrameCallback(callback); + global.__removeFrameCallback(callback); + setTimeout(() => { + expect(callCount).toBe(0); + done(); + }, defaultWaitTime); + }); + + it("should trigger the callback if it was canceled then re-scheduled", (done) => { + let callCount = 0; + const callback = () => { + callCount++; + }; + global.__postFrameCallback(callback); + global.__removeFrameCallback(callback); + global.__postFrameCallback(callback); + setTimeout(() => { + expect(callCount).toBe(1); + done(); + }, defaultWaitTime); + }); + + it("should trigger the callback if it was re-scheduled by itself", (done) => { + let callCount = 0; + const callback = () => { + callCount++; + if (callCount === 1) { + global.__postFrameCallback(callback); + } + }; + global.__postFrameCallback(callback); + setTimeout(() => { + expect(callCount).toBe(2); + done(); + }, defaultWaitTime); + }); + + it("honors the optional delay", (done) => { + const start = Date.now(); + global.__postFrameCallback(() => { + expect(Date.now() - start).not.toBeLessThan(180); + done(); + }, 200); + }); + + it("should release the callback after being done", (done) => { + let callCount = 0; + let callback = () => { + callCount++; + }; + global.__postFrameCallback(callback); + const weakCallback = new WeakRef(callback); + callback = null; + gc(); + setTimeout(() => { + gc(); + expect(callCount).toBe(1); + expect(!!weakCallback.deref()).toBe(false); + done(); + }, defaultWaitTime); + }); + + it("should release the callback removal", (done) => { + let callCount = 0; + let callback = () => { + callCount++; + }; + global.__postFrameCallback(callback); + global.__removeFrameCallback(callback); + const weakCallback = new WeakRef(callback); + callback = null; + gc(); + setTimeout(() => { + gc(); + expect(callCount).toBe(0); + expect(!!weakCallback.deref()).toBe(false); + done(); + }, defaultWaitTime); + }); + + it("should retain callback until called", (done) => { + let callCount = 0; + let callback = () => { + callCount++; + gc(); + expect(!!weakCallback.deref()).toBe(true); + }; + global.__postFrameCallback(callback); + global.__removeFrameCallback(callback); + global.__postFrameCallback(callback); + const weakCallback = new WeakRef(callback); + callback = null; + gc(); + setTimeout(() => { + gc(); + expect(callCount).toBe(1); + expect(!!weakCallback.deref()).toBe(false); + done(); + }, defaultWaitTime); + }); +}); + +describe("requestAnimationFrame", function () { + const defaultWaitTime = 300; + + it("is exposed under the standard global names", () => { + expect(typeof global.requestAnimationFrame).toBe("function"); + expect(typeof global.cancelAnimationFrame).toBe("function"); + }); + + it("throws on non-function callbacks and ignores bogus cancel handles", () => { + expect(() => global.requestAnimationFrame()).toThrowError(TypeError); + expect(() => global.requestAnimationFrame(null)).toThrowError(TypeError); + expect(() => global.requestAnimationFrame("")).toThrowError(TypeError); + expect(() => global.cancelAnimationFrame()).not.toThrow(); + expect(() => global.cancelAnimationFrame(null)).not.toThrow(); + expect(() => global.cancelAnimationFrame(-1)).not.toThrow(); + expect(() => global.cancelAnimationFrame(Number.MAX_SAFE_INTEGER)).not.toThrow(); + }); + + it("returns a handle and passes a single performance-timeline timestamp", (done) => { + const handle = global.requestAnimationFrame(function (timestamp) { + expect(arguments.length).toBe(1); + expect(typeof timestamp).toBe("number"); + const now = performance.now(); + expect(timestamp).toBeGreaterThan(0); + expect(timestamp).not.toBeGreaterThan(now); + expect(now - timestamp).toBeLessThan(250); + done(); + }); + expect(typeof handle).toBe("number"); + expect(handle).toBeGreaterThan(0); + }); + + it("runs the same function once per request", (done) => { + let callCount = 0; + const callback = () => { + callCount++; + }; + const first = global.requestAnimationFrame(callback); + const second = global.requestAnimationFrame(callback); + expect(second).not.toBe(first); + setTimeout(() => { + expect(callCount).toBe(2); + done(); + }, defaultWaitTime); + }); + + it("cancels only the cancelled request", (done) => { + let cancelledRan = false; + let keptRan = false; + const cancelled = global.requestAnimationFrame(() => { + cancelledRan = true; + }); + global.requestAnimationFrame(() => { + keptRan = true; + }); + global.cancelAnimationFrame(cancelled); + setTimeout(() => { + expect(cancelledRan).toBe(false); + expect(keptRan).toBe(true); + done(); + }, defaultWaitTime); + }); + + it("gives every callback in a batch the same timestamp", (done) => { + let firstTimestamp = null; + global.requestAnimationFrame((timestamp) => { + firstTimestamp = timestamp; + }); + global.requestAnimationFrame((timestamp) => { + expect(firstTimestamp).not.toBeNull(); + expect(timestamp).toBe(firstTimestamp); + done(); + }); + }); + + it("chains frames when the callback re-requests itself", (done) => { + const timestamps = []; + const callback = (timestamp) => { + timestamps.push(timestamp); + if (timestamps.length === 1) { + global.requestAnimationFrame(callback); + } + }; + global.requestAnimationFrame(callback); + setTimeout(() => { + expect(timestamps.length).toBe(2); + expect(timestamps[1]).not.toBeLessThan(timestamps[0]); + done(); + }, defaultWaitTime); + }); + + it("does not disturb __postFrameCallback dedupe for the same function", (done) => { + let callCount = 0; + const callback = () => { + callCount++; + }; + global.__postFrameCallback(callback); + global.requestAnimationFrame(callback); + global.__postFrameCallback(callback); + setTimeout(() => { + expect(callCount).toBe(2); + done(); + }, defaultWaitTime); + }); +}); diff --git a/TestRunner/app/tests/index.js b/TestRunner/app/tests/index.js index 5402327d..cef22a3e 100644 --- a/TestRunner/app/tests/index.js +++ b/TestRunner/app/tests/index.js @@ -138,6 +138,7 @@ require("./Modules"); require("./RuntimeImplementedAPIs"); require("./Timers"); +require("./AnimationFrameTests"); require("./EventLoopTests"); require("./URL"); diff --git a/v8ios.xcodeproj/project.pbxproj b/v8ios.xcodeproj/project.pbxproj index 2129f9fa..c3083b3e 100644 --- a/v8ios.xcodeproj/project.pbxproj +++ b/v8ios.xcodeproj/project.pbxproj @@ -22,6 +22,8 @@ 2BFE22062AC1C93100307752 /* metadata-arm64.bin in Resources */ = {isa = PBXBuildFile; fileRef = 2BFE22052AC1C93100307752 /* metadata-arm64.bin */; }; 3C1850542A6DCB2D002ACC81 /* Timers.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3C1850522A6DCB2D002ACC81 /* Timers.cpp */; }; 3C1850552A6DCB2D002ACC81 /* Timers.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3C1850532A6DCB2D002ACC81 /* Timers.hpp */; }; + 3CFCA0032E5A0001002ACC81 /* AnimationFrame.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3CFCA0012E5A0001002ACC81 /* AnimationFrame.mm */; }; + 3CFCA0042E5A0001002ACC81 /* AnimationFrame.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3CFCA0022E5A0001002ACC81 /* AnimationFrame.hpp */; }; 3C48F68D2F57905500C14231 /* json.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3C48F68B2F57905500C14231 /* json.hpp */; }; 3C5333352B0E683100BE0C47 /* Message.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3C5333332B0E683100BE0C47 /* Message.hpp */; }; 3C78BA5C2A0D600100C20A88 /* ModuleBinding.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3C78BA5A2A0D600100C20A88 /* ModuleBinding.cpp */; }; @@ -467,6 +469,8 @@ 2BFE22052AC1C93100307752 /* metadata-arm64.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; path = "metadata-arm64.bin"; sourceTree = ""; }; 3C1850522A6DCB2D002ACC81 /* Timers.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Timers.cpp; sourceTree = ""; }; 3C1850532A6DCB2D002ACC81 /* Timers.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Timers.hpp; sourceTree = ""; }; + 3CFCA0012E5A0001002ACC81 /* AnimationFrame.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AnimationFrame.mm; sourceTree = ""; }; + 3CFCA0022E5A0001002ACC81 /* AnimationFrame.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = AnimationFrame.hpp; sourceTree = ""; }; 3C48F68B2F57905500C14231 /* json.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = json.hpp; sourceTree = ""; }; 3C5333332B0E683100BE0C47 /* Message.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Message.hpp; sourceTree = ""; }; 3C78BA5A2A0D600100C20A88 /* ModuleBinding.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ModuleBinding.cpp; sourceTree = ""; }; @@ -1543,6 +1547,8 @@ 3C78BA5B2A0D600100C20A88 /* ModuleBinding.hpp */, 3C1850522A6DCB2D002ACC81 /* Timers.cpp */, 3C1850532A6DCB2D002ACC81 /* Timers.hpp */, + 3CFCA0012E5A0001002ACC81 /* AnimationFrame.mm */, + 3CFCA0022E5A0001002ACC81 /* AnimationFrame.hpp */, 3C5333332B0E683100BE0C47 /* Message.hpp */, ); path = runtime; @@ -1638,6 +1644,7 @@ C22536B8241A318900192740 /* ffi.h in Headers */, C247C16F22F82842001D2CA2 /* v8-util.h in Headers */, 3C1850552A6DCB2D002ACC81 /* Timers.hpp in Headers */, + 3CFCA0042E5A0001002ACC81 /* AnimationFrame.hpp in Headers */, C2C8EE7222CE323C001F8CEC /* ConcurrentMap.h in Headers */, C2A6EF3123745A0B00E8FBE7 /* MetadataInlines.h in Headers */, C2F4D0AE232F85E20008A2EB /* SymbolIterator.h in Headers */, @@ -2282,6 +2289,7 @@ 6573B9D4291FE29F00B0ED7C /* V8RuntimeFactory.cpp in Sources */, C2DDEBB4229EAC8300345BFE /* DictionaryAdapter.mm in Sources */, 3C1850542A6DCB2D002ACC81 /* Timers.cpp in Sources */, + 3CFCA0032E5A0001002ACC81 /* AnimationFrame.mm in Sources */, C298C027233C9AEA000DDF54 /* TSHelpers.cpp in Sources */, C2FEA16F22A3C75C00A5C0FC /* InlineFunctions.cpp in Sources */, C2DDEB9A229EAC8300345BFE /* MetadataBuilder.mm in Sources */, @@ -2929,6 +2937,8 @@ UIKit, "-framework", MobileCoreServices, + "-framework", + QuartzCore, "-L\"$(SRCROOT)/NativeScript/lib/$(CURRENT_ARCH)$(EFFECTIVE_PLATFORM_NAME)\"", "-lzip", "-lffi", @@ -3025,6 +3035,8 @@ UIKit, "-framework", MobileCoreServices, + "-framework", + QuartzCore, "-L\"$(SRCROOT)/NativeScript/lib/$(CURRENT_ARCH)$(EFFECTIVE_PLATFORM_NAME)\"", "-lzip", "-lffi",