summaryrefslogtreecommitdiff
path: root/Runtime/Input/TouchPhaseEmulation.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'Runtime/Input/TouchPhaseEmulation.cpp')
-rw-r--r--Runtime/Input/TouchPhaseEmulation.cpp719
1 files changed, 719 insertions, 0 deletions
diff --git a/Runtime/Input/TouchPhaseEmulation.cpp b/Runtime/Input/TouchPhaseEmulation.cpp
new file mode 100644
index 0000000..be1ab28
--- /dev/null
+++ b/Runtime/Input/TouchPhaseEmulation.cpp
@@ -0,0 +1,719 @@
+#include "UnityPrefix.h"
+
+/*
+ * TouchPhaseEmulation is layer between an 'event-based' touch OS (Android, Metro etc) and our phase-based script API (similar to iOS).
+ *
+ * The core logic of this layer is in DispatchTouchEvent, which handles mapping and collapsing of events to a frame-discrete phase.
+ */
+
+#define DEBUG_TOUCH_EMU (DEBUGMODE && 0)
+
+#include "TouchPhaseEmulation.h"
+
+#if UNITY_BLACKBERRY
+ static const int touchTimeout = 400;
+#else
+ static const int touchTimeout = 150;
+#endif
+
+// *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
+
+class TouchImpl : public Touch
+{
+ enum { kEmptyTouchId = ~0UL };
+
+public:
+ TouchImpl ()
+ {
+ clear ();
+ }
+
+ long long timestamp; // in milliseconds
+ UInt32 pointerId; // matches OS pointerId
+ size_t frameToReport; // frame # for this event to be reported. Should
+ // only be =gFrameCount or =gFrameCount+1
+ size_t frameBegan; // frame # when BEGIN event was received
+ UInt32 endPhaseInQueue; // acts both as a bool to indicate that this event
+ // has already received an END/CANCEL from OS and
+ // will be reported to scripts next frame, and as
+ // a container to hold the actual value: was it
+ // END or CANCEL?
+
+ void setDeltaTime (long long newTimestamp)
+ {
+ if (timestamp == 0)
+ return;
+
+ deltaTime = (newTimestamp - timestamp) / 1000.0f;
+ }
+
+ void setDeltaPos (Vector2f const& newPos)
+ {
+ if (CompareApproximately (pos, Vector2f::zero))
+ return;
+
+ deltaPos = newPos - pos;
+ }
+
+ bool isMultitap (long long newTimestamp, Vector2f const& newPos, float screenDPI)
+ {
+ static const float tapZoneRadiusCM = 0.4f; // 4mm
+ static const float cmToInch = 0.393701f;
+ static const float multitapRadiusPixels = screenDPI * tapZoneRadiusCM * cmToInch;
+ static const float multitapRadiusSqr = multitapRadiusPixels * multitapRadiusPixels;
+
+ return newTimestamp - timestamp < touchTimeout
+ && SqrMagnitude (pos - newPos) < multitapRadiusSqr;
+ }
+
+ void setTapCount (long long newTimestamp, Vector2f const &newPos, float screenDPI)
+ {
+ if (isMultitap (newTimestamp, newPos, screenDPI))
+ ++tapCount;
+ else
+ tapCount = 1;
+ }
+
+ void init(size_t _pointerId, Vector2f _pos, TouchPhaseEmulation::TouchPhase _phase,
+ long long _timestamp, size_t currFrame)
+ {
+ pointerId = _pointerId;
+ pos = _pos;
+ rawPos = _pos;
+ phase = _phase;
+ frameBegan = currFrame;
+ timestamp = _timestamp;
+ frameToReport = currFrame;
+ }
+
+ void clear ()
+ {
+ id = kEmptyTouchId;
+ phase = TouchPhaseEmulation::kTouchCanceled;
+ endPhaseInQueue = 0;
+ deltaPos = Vector2f (0.0f, 0.0f);
+ deltaTime = 0.0f;
+ frameToReport = 0;
+ frameBegan = 0;
+ tapCount = 0;
+ rawPos = pos = Vector2f (0.0f, 0.0f);
+ timestamp = 0;
+ pointerId = kEmptyTouchId;
+ }
+
+ bool isOld (size_t frame) const
+ {
+ return frameToReport < frame;
+ }
+
+ bool isEmpty () const
+ {
+ return id == kEmptyTouchId;
+ }
+
+ bool isFinished () const
+ {
+ return !isEmpty () && IsEnd (phase);
+ }
+
+ bool willBeFinishedNextFrame () const
+ {
+ return !isEmpty () && IsEnd (endPhaseInQueue);
+ }
+
+ bool isNow (size_t frame) const
+ {
+ return frameToReport == frame;
+ }
+
+ static bool IsBegin (size_t phase)
+ {
+ return phase == TouchPhaseEmulation::kTouchBegan;
+ }
+
+ static bool IsTransitional (size_t phase)
+ {
+ return phase == TouchPhaseEmulation::kTouchMoved || phase == TouchPhaseEmulation::kTouchStationary;
+ }
+
+ static bool IsEnd (size_t phase)
+ {
+ return phase == TouchPhaseEmulation::kTouchEnded || phase == TouchPhaseEmulation::kTouchCanceled;
+ }
+
+
+#if DEBUG_TOUCH_EMU
+ void dump (TouchImpl* gAllTouches)
+ {
+ size_t index = this - gAllTouches;
+ char const* phaseNames[] = {
+ "<Began>",
+ "<Moved>",
+ "<Stationary>",
+ "<Ended>",
+ "<Canceled>",
+ };
+
+ char const *fmt =
+ "T[%02d]={fid=%d, pid=%d, phase=%s, p=(%3.1f,%3.1f), dp=(%3.1f,%3.1f),\n"
+ " tm=%lld, dtm=%f, endPhaseQ=%d, fbeg=%d, frep=%d, tcnt=%d}\n";
+
+ printf_console (fmt, index, id, pointerId, phaseNames[phase], pos.x, pos.y,
+ deltaPos.x, deltaPos.y, timestamp, deltaTime,
+ endPhaseInQueue, frameBegan, frameToReport, tapCount);
+ }
+#endif
+};
+
+// *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
+
+TouchPhaseEmulation::TouchPhaseEmulation(float screenDPI, bool singleTouchDevice)
+: m_AllocatedFingerIDs(0)
+, m_FrameCount(0)
+, m_ScreenDPI(screenDPI)
+, m_IsMultiTouchEnabled(!singleTouchDevice)
+, m_IsSingleTouchDevice(singleTouchDevice)
+{
+ m_TouchSlots = new TouchImpl[kMaxTouchCount];
+ InitTouches();
+}
+
+TouchPhaseEmulation::~TouchPhaseEmulation()
+{
+ delete [] m_TouchSlots;
+}
+
+void TouchPhaseEmulation::InitTouches ()
+{
+ for (size_t i = 0; i < kMaxTouchCount; ++i)
+ {
+ m_TouchSlots[i].clear();
+ }
+ m_AllocatedFingerIDs = 0;
+
+ m_FrameCount = 1;
+}
+
+void TouchPhaseEmulation::PreprocessTouches ()
+{
+ DiscardRedundantTouches();
+}
+
+void TouchPhaseEmulation::PostprocessTouches ()
+{
+#if DEBUG_TOUCH_EMU
+ DumpAll ();
+#endif
+ ++m_FrameCount;
+ UpdateActiveTouches();
+}
+
+
+bool TouchPhaseEmulation::IsExistingTouch( int pointerId )
+{
+ TouchImpl* matchingSlots[kMaxTouchCount];
+ const size_t slotsFound = FindByPointerId(matchingSlots, pointerId);
+
+ for (size_t i = 0; i < slotsFound; ++i)
+ if (matchingSlots[i])
+ return true;
+
+ return false;
+}
+
+void TouchPhaseEmulation::AddTouchEvent (int pointerId, float x, float y, TouchPhase newPhase, long long timestamp)
+{
+ Vector2f pos = Vector2f (x, y);
+
+#if UNITY_WINRT
+ // [Metro] With one finger touching; that touch pointerId is usually > 0
+ if (!m_IsMultiTouchEnabled && GetTouchCount() > 0 && !IsExistingTouch(pointerId))
+ return;
+#else
+ if (!m_IsMultiTouchEnabled && pointerId > 0)
+ return;
+#endif
+
+ DispatchTouchEvent (pointerId, pos, static_cast<TouchPhase> (newPhase), timestamp, m_FrameCount);
+}
+
+void TouchPhaseEmulation::DispatchTouchEvent (size_t pointerId, Vector2f pos, TouchPhase newPhase, long long timestamp, size_t currFrame)
+{
+ // Brief terminology legend:
+ // action The OS level indication of what happened in a particular touch event; DOWN, MOVE, UP etc.
+ // phase Logical state of a touch, emulated to the script side; Began/Moved/Ended.
+ // Phase is considered constant per frame, and all intra-frame actions are collapsed to a single phase.
+ // If two actions can't be collapsed (like UP/DOWN), then the DOWN action will be delayed one frame.
+ // pointerId The ID given to a touch event by the OS. These IDs can be reused if touches end/start within the same frame.
+ // fingerId The ID we give a touch when presented to the script side. Also known as 'id' in the Touch struct.
+ // touch event OS level touch information
+ // touch slot Script level touch information.
+
+ // Step 1: Determine if already tracking this 'pointerId'
+ // We do this by looping through all touch slots while looking for an active touch with matching 'pointerId'.
+ // If a slot with phase == Ended has been inactive for 150ms => set it to Inactive
+ // Step 2: Determine if an old touch slot should be updated with the new event information, or if a new touch slot should be allocated.
+ // If no touch slot was found in Step 1 => allocate a new touch slot. Skip to step 4.
+ // If only a singular touch slot was found in Step 1 => use that slot. Skip to step 4.
+ // If multiple slots were found in Step 1 => determine which slot to use, or if another slot needs to be allocated.
+ // Step 3: Determine which active slot to use, or allocate a new.
+ // If phase == Began : for each slot with phase == Ended, consider event to be a multi-tap by comparing position and timestamp.
+ // If phase != Began : find slot with phase != Ended (should only be one!)
+ // If no slot matches : allocate a new slot with fingerId = highestFingerIdUsed + 1.
+ // Step 4: Initialize/update the touch slot based on current and old touch phase.
+ // If new phase == Began and old phase == Ended => increase tap count.
+ // If new phase == Began => try to compact finger id
+ // If new phase == Ended and old phase == Began on the same frame => delay new phase (Ended) until next frame.
+ // If new phase == Moved and old phase == Stationary => check distance against threshold for Moved/Stationary.
+ //
+ // After every frame, loop through all active touch slots:
+ // If a delayed phase (Ended) was enabled => update the phase.
+ // If a touch != Ended wasn't updated this frame => set phase = Stationary
+ // If a touch began and ended the this frame, and it resulted in another, currently active, touch having an increased tap count
+ // => clear those extra touches, and compact the finger id
+
+ // NB: for now we do use passed position as both raw position and position
+ // on ios, where we actually implement raw position, different code is used
+
+ FreeExpiredTouches(m_FrameCount, timestamp);
+
+ TouchImpl* matchingSlots[kMaxTouchCount];
+ const size_t slotsFound = FindByPointerId(matchingSlots, pointerId);
+
+ TouchImpl* touch = NULL;
+ int inheritedTapCount = 0;
+
+#if UNITY_WINRT
+ // [Metro] Calculate tapCount manually as pointerIds aren't reused.
+ if (TouchImpl::IsBegin(newPhase))
+ inheritedTapCount += CalculateTapCount(timestamp, pos);
+#endif
+
+ for (size_t i = 0; i < slotsFound; ++i)
+ {
+ TouchImpl* slot = matchingSlots[i];
+
+ bool touchFinished = slot->isFinished() || slot->willBeFinishedNextFrame();
+ if (TouchImpl::IsBegin(newPhase))
+ {
+ if (touchFinished)
+ {
+ if (slot->isOld(m_FrameCount))
+ {
+ touch = slot;
+ }
+
+ if (slot->isMultitap(timestamp, pos, m_ScreenDPI))
+ {
+ inheritedTapCount = slot->tapCount;
+ }
+ }
+ }
+ else
+ {
+ if (!touchFinished)
+ {
+ if (touch)
+ {
+ if (DEBUGMODE) printf_console("Stale/stuck touch released.");
+ ExpireOld(*touch);
+ }
+ touch = slot;
+ }
+ }
+ }
+
+ if (!touch)
+ {
+ if (!TouchImpl::IsBegin(newPhase))
+ {
+ if (DEBUGMODE) printf_console("Dropping touch event part of canceled gesture.");
+ return;
+ }
+ if (!(touch = AllocateNew()))
+ return;
+ }
+
+ if (TouchImpl::IsBegin (newPhase))
+ {
+ touch->tapCount = inheritedTapCount;
+ #if DEBUG_TOUCH_EMU
+ printf_console("Slot before initialized:");
+ touch->dump(m_TouchSlots);
+ #endif
+
+ touch->init( pointerId, pos, newPhase, timestamp, currFrame );
+ touch->setTapCount( timestamp, pos, m_ScreenDPI );
+ touch->id = CompactFingerID(touch->id);
+
+ #if DEBUG_TOUCH_EMU
+ printf_console("Slot initialized:");
+ touch->dump(m_TouchSlots);
+ #endif
+ return;
+ }
+ else if (TouchImpl::IsEnd (newPhase))
+ {
+ // if touch began this frame, we will delay end phase for one frame
+ if (touch->frameBegan == currFrame)
+ touch->endPhaseInQueue = newPhase;
+ else
+ touch->phase = newPhase;
+
+ // Android only sends CANCELED on the first pointer in a gesture - kill all other active touches when this happens.
+ if (newPhase == kTouchCanceled)
+ {
+ for (size_t i = 0; i < kMaxTouchCount; ++i)
+ {
+ TouchImpl* slot = &m_TouchSlots[i];
+ if (slot->isEmpty() || slot->isFinished() || slot->willBeFinishedNextFrame())
+ continue;
+ slot->endPhaseInQueue = newPhase;
+ #if DEBUG_TOUCH_EMU
+ m_TouchSlots[i].dump(m_TouchSlots);
+ #endif
+ }
+ }
+ }
+ else if (newPhase == kTouchMoved
+ && touch->phase == kTouchStationary)
+ {
+ // old event is STATIONARY, the new one is MOVE. Android does not
+ // report STATIONARY Events, so if MOVE's deltaPos is not big enough,
+ // let's keep it STATIONARY. Promote to MOVE otherwise.
+ static const float deltaPosTolerance = 0.5f;
+ if (Magnitude (touch->pos - pos) >= deltaPosTolerance)
+ touch->phase = newPhase;
+ }
+
+ touch->setDeltaPos (pos);
+ touch->pos = pos;
+
+ touch->setDeltaTime (timestamp);
+ touch->timestamp = timestamp;
+ touch->frameToReport = currFrame;
+
+#if DEBUG_TOUCH_EMU
+ printf_console("Slot updated:");
+ touch->dump(m_TouchSlots);
+#endif
+
+}
+
+size_t TouchPhaseEmulation::FindByPointerId(TouchImpl* matchingSlots[kMaxTouchCount], size_t pointerId)
+{
+#if DEBUG_TOUCH_EMU
+ printf_console("%s", __FUNCTION__);
+#endif
+ size_t slotsFound = 0;
+ for (size_t i = 0; i < kMaxTouchCount; ++i)
+ {
+ if (m_TouchSlots[i].pointerId != pointerId)
+ continue;
+#if DEBUG_TOUCH_EMU
+ m_TouchSlots[i].dump(m_TouchSlots);
+#endif
+ matchingSlots[slotsFound++] = &m_TouchSlots[i];
+ }
+ return slotsFound;
+}
+
+TouchImpl* TouchPhaseEmulation::AllocateNew()
+{
+#if DEBUG_TOUCH_EMU
+ printf_console("%s", __FUNCTION__);
+#endif
+ // allocate virtual fingerId
+ int fingerId = 0;
+ const int maxFingerId = sizeof(m_AllocatedFingerIDs) * 8;
+ for (; fingerId < maxFingerId; ++fingerId)
+ {
+ UInt32 bitField = (1 << fingerId);
+ if (m_AllocatedFingerIDs & bitField)
+ continue;
+ m_AllocatedFingerIDs |= bitField;
+ break;
+ }
+ if (fingerId >= maxFingerId)
+ {
+ Assert (!"Out of virtual finger IDs!");
+ return NULL;
+ }
+
+ // find empty slot for touch
+ for (size_t i = 0; i < kMaxTouchCount; ++i)
+ {
+ TouchImpl& t = m_TouchSlots[i];
+
+ if (!t.isEmpty())
+ continue;
+
+ t.id = fingerId;
+ t.deltaPos = Vector2f(0, 0);
+ t.deltaTime = 0.0f;
+ t.endPhaseInQueue = 0;
+
+ return &t;
+ }
+
+ Assert (!"Out of free touches!");
+ return NULL;
+}
+
+void TouchPhaseEmulation::ExpireOld(TouchImpl& touch)
+{
+#if DEBUG_TOUCH_EMU
+ printf_console("%s", __FUNCTION__);
+#endif
+
+ if (touch.isEmpty())
+ {
+ ErrorString("Trying to expire empty touch slot!");
+ return;
+ }
+
+ // deallocate virtual fingerId
+ UInt32 bitField = (1 << touch.id);
+ Assert((m_AllocatedFingerIDs & bitField) && "Touch with stale finger ID killed!");
+ m_AllocatedFingerIDs &= ~bitField;
+
+ Assert(!touch.endPhaseInQueue && "Delayed touch killed prematurely!");
+ touch.clear();
+}
+
+int TouchPhaseEmulation::CompactFingerID(int id)
+{
+ int fingerId = 0;
+ const int maxFingerId = sizeof(m_AllocatedFingerIDs) * 8;
+ for (; fingerId < maxFingerId; ++fingerId)
+ {
+ UInt32 bitField = (1 << fingerId);
+ if (m_AllocatedFingerIDs & bitField)
+ continue;
+
+ if (id < fingerId)
+ return id;
+
+ m_AllocatedFingerIDs |= bitField;
+ bitField = (1 << id);
+ Assert((m_AllocatedFingerIDs & bitField) && "Touch with stale finger ID killed!");
+ m_AllocatedFingerIDs &= ~bitField;
+ id = fingerId;
+ break;
+ }
+ return id;
+}
+
+void TouchPhaseEmulation::FreeExpiredTouches (size_t eventFrame, long long timestamp)
+{
+ for (size_t i = 0; i < kMaxTouchCount; ++i)
+ {
+ TouchImpl& touch = m_TouchSlots[i];
+
+ if (touch.isEmpty())
+ continue;
+
+ long long age = timestamp - touch.timestamp;
+ if (touch.isOld(eventFrame) && touch.isFinished() && age > touchTimeout)
+ {
+ ExpireOld(touch);
+ }
+ }
+}
+
+void TouchPhaseEmulation::DiscardRedundantTouches()
+{
+ for (size_t i = 0; i < kMaxTouchCount; ++i)
+ {
+ TouchImpl* t0 = &m_TouchSlots[i];
+ TouchImpl* t1 = 0;
+
+ if (t0->isEmpty())
+ continue;
+
+ // find Down/Up touches recorded within a single frame
+ bool downUpTouch =
+ t0->frameBegan == m_FrameCount &&
+ t0->frameToReport == m_FrameCount &&
+ t0->willBeFinishedNextFrame() &&
+ !t0->isFinished();
+
+ if (!downUpTouch)
+ continue;
+
+ #if DEBUG_TOUCH_EMU
+ printf_console("Found new touch set to expire next frame");
+ t0->dump(m_TouchSlots);
+ #endif
+
+ bool redundant = false;
+
+ // compare the multitap info
+ for (size_t j = 0; j < kMaxTouchCount; ++j)
+ {
+ t1 = &m_TouchSlots[j];
+
+ if (t1->isEmpty() || i == j)
+ continue;
+
+ bool multitapTouch =
+ t1->frameBegan == m_FrameCount &&
+ t1->frameToReport == m_FrameCount &&
+ t1->pointerId == t0->pointerId &&
+ t1->tapCount > t0->tapCount &&
+ t0->isMultitap(t1->timestamp, t1->pos, m_ScreenDPI) &&
+ !t1->isFinished();
+
+ if (!multitapTouch)
+ continue;
+
+ #if DEBUG_TOUCH_EMU
+ printf_console("Found new touch, with tapCount that matches the one found earlier");
+ t1->dump(m_TouchSlots);
+ #endif
+ // found a match
+ redundant = true;
+ break;
+ }
+
+ if (redundant)
+ {
+ t0->endPhaseInQueue = 0; // this touch is officially gone anyway
+ ExpireOld(*t0);
+ t1->id = CompactFingerID(t1->id);
+ #if DEBUG_TOUCH_EMU
+ printf_console("New touch, with compacted finger id");
+ t1->dump(m_TouchSlots);
+ #endif
+ }
+ else
+ {
+ t0->id = CompactFingerID(t0->id);
+ #if DEBUG_TOUCH_EMU
+ printf_console("Refresh touch, with compacted finger id");
+ t0->dump(m_TouchSlots);
+ #endif
+ }
+ }
+}
+
+void TouchPhaseEmulation::UpdateActiveTouches()
+{
+ for (size_t i = 0; i < kMaxTouchCount; ++i)
+ {
+ TouchImpl& touch = m_TouchSlots[i];
+
+ if (touch.isEmpty())
+ continue;
+
+ // Skip Expired Touches
+ if (touch.isFinished())
+ {
+ continue;
+ }
+
+ // End Delayed Touches
+ if (touch.willBeFinishedNextFrame ())
+ {
+ touch.deltaPos = Vector2f (0, 0);
+ touch.phase = touch.endPhaseInQueue;
+ touch.endPhaseInQueue = 0;
+ touch.frameToReport = m_FrameCount;
+ continue;
+ }
+
+ // Default Stationary Touches
+ touch.phase = kTouchStationary;
+ touch.deltaPos = Vector2f (0, 0);
+ touch.frameToReport = m_FrameCount;
+ }
+
+}
+
+size_t TouchPhaseEmulation::GetTouchCount ()
+{
+ size_t count = 0;
+
+ // TODO: on first call to GetTouchCount() per frame, call PackTouchIds() to
+ // compact virtual touch IDs to stand out less
+
+ for (size_t i = 0; i < kMaxTouchCount; ++i)
+ if ( m_TouchSlots[i].isNow(m_FrameCount) &&
+ !m_TouchSlots[i].isEmpty() )
+ ++count;
+
+ return count;
+}
+
+size_t TouchPhaseEmulation::GetActiveTouchCount ()
+{
+ size_t count = 0;
+
+ for (size_t i = 0; i < kMaxTouchCount; ++i)
+ if (!m_TouchSlots[i].isEmpty() && !m_TouchSlots[i].isFinished())
+ ++count;
+
+ return count;
+}
+
+// @param index Zero-based index of events that are to be reported this
+// frame. Since not all of the events in the container need to
+// be reported this frame, it's used to skip already reported
+// ones.
+bool TouchPhaseEmulation::GetTouch (size_t index, Touch& touch)
+{
+ for (size_t i = 0; i < kMaxTouchCount; ++i)
+ {
+ if ( m_TouchSlots[i].isNow(m_FrameCount) &&
+ !m_TouchSlots[i].isEmpty() &&
+ index-- == 0 )
+ {
+ touch = m_TouchSlots[i];
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool TouchPhaseEmulation::IsMultiTouchEnabled ()
+{
+ if (m_IsSingleTouchDevice)
+ return false;
+
+ return m_IsMultiTouchEnabled;
+}
+
+void TouchPhaseEmulation::SetMultiTouchEnabled (bool enabled)
+{
+ if (m_IsSingleTouchDevice)
+ return;
+
+ m_IsMultiTouchEnabled = enabled;
+}
+
+int TouchPhaseEmulation::CalculateTapCount( long long timestamp, Vector2f const &pos ) const
+{
+ int result = 0;
+ for (size_t i = 0; i < kMaxTouchCount; ++i)
+ {
+ TouchImpl& touch = m_TouchSlots[i];
+
+ if (touch.isEmpty())
+ continue;
+
+ if (touch.isMultitap(timestamp, pos, m_ScreenDPI))
+ result += touch.tapCount;
+ }
+
+ return result;
+}
+
+#if DEBUG_TOUCH_EMU
+void TouchPhaseEmulation::DumpAll (bool verbose)
+{
+ for (int i = 0; i < kMaxTouchCount; ++i)
+ if (!m_TouchSlots[i].isEmpty() && !m_TouchSlots[i].isOld(m_FrameCount) || verbose)
+ m_TouchSlots[i].dump( m_TouchSlots );
+}
+#endif